Initial commit

This commit is contained in:
Christian Franz 2022-01-19 20:55:23 +01:00
commit 929a188497
57 changed files with 17951 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

BIN
Doc/DML Documentation.pdf Normal file

Binary file not shown.

455
modules/FARPZones.lua Normal file
View File

@ -0,0 +1,455 @@
FARPZones = {}
FARPZones.version = "1.0.2"
--[[--
Version History
1.0.0 - Initial Version
1.0.1 - support "none" as defender types
- default types for defenders to none
1.0.2 - hiddenRed, hiddenBlue, hiddenGrey
--]]--
FARPZones.requiredLibs = {
"dcsCommon", -- common is of course needed for everything
-- pretty stupid to check for this since we
-- need common to invoke the check, but anyway
"cfxZones", -- Zones, of course
-- "cfxCommander", -- to make troops do stuff
-- "cfxGroundTroops", -- generic when dropping troops
}
-- *** DOES NOT EXTEND ZONES, USES OWN STRUCT ***
-- *** SETS ZONE.OWNER IF PRESENT, POSSIBLE CONFLICT
-- *** WITH OWNED ZONES
-- *** DOES NOT WORK WITH AIRFIELDS!
-- *** USE OWNED ZONES AND SPAWNZONES FOR AIRFIELDS
--[[--
Functioning, capturable FARPS with all services. To use,
place a FARP on a map, an a Zone that contains the FARP,
then add the following attributes
Z O N E A T T R I B U T E S
- FARP <anything>: indicate that this is a FARP ZONE. Must
contain at least one FARP inside the zone. The first FARP
found will become main FARP to determine ownership
- rPhiHDef - r, phi, heading separated by coma, eg
<120, 245, 0> that defines radius and Phi (in degrees) for
the center position, and heading for the group of defenders
for this FARP. r, phi relative to ZONE center, not FARP
- redDefenders - Type Strings for defender vehicles, coma
separated. e.g. "BTR-80,BTR-80" will create two BTR-80
vehicles when owned by RED
- blueDefenders - Type Strings for defender vehicles, coma
separated when owned by blue
- formation - formation for defenders, e.g. "grid".
optional, defaults to "circle-out". Span raidus is 100m
- rPhiHRes - r, phi and H separated by coma to determine
the location and heading of FARP Resopurces (services).
They always auto-gen all vehicles for all services. They
always spawn as "line_V" through center with radius 50 meters
Optional. Will spawn around zone center else. Remember that
all these vehicles MUST be within 150m of FARP Center
- hidden - if true, no circle on map, else (default) visible
to all with owner color
--]]--
FARPZones.resourceTypes = {
"M978 HEMTT Tanker", -- BLUE fuel
"M 818", -- BLUE ammo
"M 818", -- Blue Power (repair???)
"Hummer", -- BLUE ATC
"ATMZ-5", -- RED refuel
"KAMAZ Truck", -- rearming
"SKP-11", -- communication
"ZiL-131 APA-80", -- Power
}
FARPZones.spinUpDelay = 30 -- seconds until FARP becomes operational after capture
FARPZones.allFARPZones = {}
FARPZones.startingUp = false
-- FARP ZONE ACCESS
function FARPZones.addFARPZone(aFARP)
FARPZones.allFARPZones[aFARP.zone] = aFARP
end
function FARPZones.removeFARPZone(aFARP)
FARPZones.allFARPZones[aFARP.zone] = nil
end
function FARPZones.getFARPForZone(aZone)
return FARPZones.allFARPZones[aZone]
end
function FARPZones.getFARPZoneForFARP(aFarp)
-- find the first FARP zone that associates with
-- aFARP (an airField)
for idx, aFarpZone in pairs(FARPZones.allFARPZones) do
local associatedFarps = aFarpZone.myFarps
for itoo, assocF in pairs(associatedFarps) do
if assocF == aFarp then return aFarpZone end
end
end
return nil
end
function FARPZones.createFARPFromZone(aZone)
-- WARNING: WILL SET ZONE.OWNER
local theFarp = {}
theFarp.zone = aZone
theFarp.name = aZone.name
-- find the FARPS that belong to this zone
local thePoint = aZone.point
local mapFarps = dcsCommon.getAirbasesInRangeOfPoint(
thePoint,
aZone.radius,
1 -- FARPS = Helipads
)
-- only #1 is significant for owner
theFarp.myFarps = mapFarps
theFarp.owner = 0 -- start with neutral
aZone.owner = 0
if #mapFarps == 0 then
trigger.action.outText("***Farp Zones: no FARP found for zone " .. aZone.name, 30)
else
for idx, aFarp in pairs(mapFarps) do
-- trigger.action.outText("Associated FARP " .. aFarp:getName() .. " with FARP Zone " .. aZone.name, 30)
end
theFarp.mainFarp = theFarp.myFarps[1]
theFarp.point = theFarp.mainFarp:getPoint() -- this is FARP, not zone!!!
theFarp.owner = theFarp.mainFarp:getCoalition()
aZone.owner = theFarp.owner
end
-- get r and phi for defenders
local rPhi = cfxZones.getVectorFromZoneProperty(
aZone,
"rPhiHDef",
3)
-- trigger.action.outText("*** DEF rPhi are " .. rPhi[1] .. " and " .. rPhi[2], 30)
-- get r and phi for facilities
-- create a new defenderzone for this
local r = rPhi[1]
local phi = rPhi[2] * 0.0174533 -- 1 degree = 0.0174533 rad
local dx = aZone.point.x + r * math.cos(phi)
local dz = aZone.point.z + r * math.sin(phi)
theFarp.defZone = cfxZones.createSimpleZone(aZone.name .. "-Def", {x=dx, y = 0, z=dz}, 100)
theFarp.defHeading = rPhi[3]
rPhi = cfxZones.getVectorFromZoneProperty(
aZone,
"rPhiHRes",
3) -- optional, will reterurn {0,0} else
-- trigger.action.outText("*** RES rPhi are " .. rPhi[1] .. " and " .. rPhi[2] .. " heading " .. rPhi[3], 30)
r = rPhi[1]
phi = rPhi[2] * 0.0174533 -- 1 degree = 0.0174533 rad
dx = aZone.point.x + r * math.cos(phi)
dz = aZone.point.z + r * math.sin(phi)
theFarp.resZone = cfxZones.createSimpleZone(aZone.name .. "-Res", {x=dx, y = 0, z=dz}, 50)
theFarp.resHeading = rPhi[3]
-- get redDefenders - defenders produced when red owned
theFarp.redDefenders = cfxZones.getStringFromZoneProperty(aZone, "redDefenders", "none")
-- get blueDefenders - defenders produced when blue owned
theFarp.blueDefenders = cfxZones.getStringFromZoneProperty(aZone, "blueDefenders", "none")
-- get formation for defenders
theFarp.formation = cfxZones.getStringFromZoneProperty(aZone, "formation", "circle_out")
theFarp.count = 0 -- for uniqueness
theFarp.hideRed = cfxZones.getBoolFromZoneProperty(aZone, "hideRed")
theFarp.hideBlue = cfxZones.getBoolFromZoneProperty(aZone, "hideBlue")
theFarp.hideGrey = cfxZones.getBoolFromZoneProperty(aZone, "hideGrey")
theFarp.hidden = cfxZones.getBoolFromZoneProperty(aZone, "hidden")
return theFarp
end
--[[--
function FARPZones.drawFarp(theFarp)
local theZone = theFarp.zone
local theOwner = theFarp.owner
FARPZones.drawZoneInMap(theZone, theOwner)
end
--]]--
function FARPZones.drawFARPCircleInMap(theFarp)
if not theFarp then return end
if theFarp.zone and theFarp.zone.markID then
-- remove previous mark
trigger.action.removeMark(theFarp.zone.markID)
theFarp.zone.markID = nil
end
if theFarp.hideRed and
theFarp.owner == 1 then
-- hide only when red
return
end
if theFarp.hideBlue and
theFarp.owner == 2 then
-- hide only when blue
return
end
if theFarp.hideGrey and
theFarp.owner == 0 then
-- hide only when blue
return
end
if theFarp.hidden then
return
end
-- owner is 0 = neutral, 1 = red, 2 = blue
-- will save markID in zone's markID
-- should be able to only show owned
-- draws 2km radius circle around main (first) FARP
local aZone = theFarp.zone
local thePoint = theFarp.point
local owner = theFarp.owner
local lineColor = {1.0, 0, 0, 1.0} -- red
local fillColor = {1.0, 0, 0, 0.2} -- red
if owner == 2 then
lineColor = {0.0, 0, 1.0, 1.0}
fillColor = {0.0, 0, 1.0, 0.2}
elseif owner == 0 then
lineColor = {0.8, 0.8, 0.8, 1.0}
fillColor = {0.8, 0.8, 0.8, 0.2}
end
local theShape = 2 -- circle
local markID = dcsCommon.numberUUID()
trigger.action.circleToAll(-1, markID, thePoint, 2000, lineColor, fillColor, 1, true, "")
aZone.markID = markID
end
function FARPZones.drawZoneInMap(aZone, owner)
-- owner is 0 = neutral, 1 = red, 2 = blue
-- will save markID in zone's markID
-- should be moved to cfxZones
-- should be able to only show owned
if aZone.markID then
trigger.action.removeMark(aZone.markID)
end
local lineColor = {1.0, 0, 0, 1.0} -- red
local fillColor = {1.0, 0, 0, 0.2} -- red
if owner == 2 then
lineColor = {0.0, 0, 1.0, 1.0}
fillColor = {0.0, 0, 1.0, 0.2}
elseif owner == 0 then
lineColor = {0.8, 0.8, 0.8, 1.0}
fillColor = {0.8, 0.8, 0.8, 0.2}
end
local theShape = 2 -- circle
local markID = dcsCommon.numberUUID()
trigger.action.circleToAll(-1, markID, aZone.point, aZone.radius, lineColor, fillColor, 1, true, "")
aZone.markID = markID
end
function FARPZones.scheduedProduction(args)
-- args contain [aFarp, owner]
-- make sure that owner is still the same
-- and if so, branch to produce vehicles
-- ***write-though to zone.owner
local theFarp = args[1]
local owner = args[2]
-- make sure the farp wasn't conquered in the meantime
if owner == theFarp.mainFarp:getCoalition() then
-- ok, still same owner , go ahead and spawn
theFarp.owner = owner
theFarp.zone.owner = owner
FARPZones.produceVehicles(theFarp)
trigger.action.outTextForCoalition(theFarp.owner, "FARP " .. theFarp.name .. " has become operational!", 30)
trigger.action.outSoundForCoalition(theFarp.owner, "Quest Snare 3.wav")
end
end
function FARPZones.produceVehicles(theFarp)
-- first, remove anything that may still be there
if theFarp.defenders and theFarp.defenders:isExist() then
theFarp.defenders:destroy()
end
if theFarp.resources and theFarp.resources:isExist() then
theFarp.resources:destroy()
end
theFarp.defenders = nil
theFarp.resources = nil
-- spawn defenders
local owner = theFarp.owner -- coalition
local theTypes = theFarp.redDefenders
if owner == 2 then theTypes = theFarp.blueDefenders end
local unitTypes = dcsCommon.splitString(theTypes, ",")
if #unitTypes < 1 then
table.insert(unitTypes, "Soldier M4") -- make it one m4 trooper as fallback
end
-- trigger.action.outText("*** ENTER produce vehicles, will produce " .. theTypes , 30)
local theCoalition = theFarp.owner
if theTypes ~= "none" then
local theGroup = cfxZones.createGroundUnitsInZoneForCoalition (
theCoalition,
theFarp.name .. "-D" .. theFarp.count, -- must be unique
theFarp.defZone,
unitTypes,
theFarp.formation,
theFarp.defHeading)
-- we do not add these troops to ground troop management
theFarp.defenders = theGroup -- but we retain a handle just in case
end
unitTypes = FARPZones.resourceTypes
local theGroup = cfxZones.createGroundUnitsInZoneForCoalition (
theCoalition,
theFarp.name .. "-R" .. theFarp.count, -- must be unique
theFarp.resZone,
unitTypes,
"line_v",
theFarp.resHeading)
theFarp.resources = theGroup
-- update unique counter
theFarp.count = theFarp.count + 1
end
--
-- EVENT PROCESSING
--
FARPZones.myEvents = {10, } -- 10: S_EVENT_BASE_CAPTURED
function FARPZones.isInteresting(eventID)
-- return true if we are interested in this event, false else
for key, evType in pairs(FARPZones.myEvents) do
if evType == eventID then return true end
end
return false
end
function FARPZones.preProcessor(event)
if not event then return false end
if not event.place then return false end
return FARPZones.isInteresting(event.id)
end
function FARPZones.postProcessor(event)
-- don't do anything
end
function FARPZones.somethingHappened(event)
-- *** writes to zone.owner
local theUnit = event.initiator
local ID = event.id
trigger.action.outText("FZ: something happened", 30)
local aFarp = event.place
local zonedFarp = FARPZones.getFARPZoneForFARP(aFarp)
if not zonedFarp then
trigger.action.outText("Hand change, NOT INTERESTING", 30)
return
end
local newOwner = aFarp:getCoalition()
local blueRed = "Red"
if newOwner == 2 then blueRed = "Blue" end
trigger.action.outText("FARP " .. zonedFarp.zone.name .. " captured by " .. blueRed .."!", 30)
trigger.action.outSound("Quest Snare 3.wav")
zonedFarp.owner = newOwner
zonedFarp.zone.owner = newOwner
-- better: sound winm and lose to different sides
-- update color in map
-- FARPZones.drawFarp(zonedFarp)
FARPZones.drawFARPCircleInMap(zonedFarp)
-- remove all existing resources immediately,
-- no more service available
if zonedFarp.resources and zonedFarp.resources:isExist() then
zonedFarp.resources:destroy()
zonedFarp.resources = nil
end
-- now schedule operational after spin-up delay
timer.scheduleFunction(
FARPZones.scheduedProduction,
{zonedFarp, newOwner}, -- pass farp struct and current owner
timer.getTime() + FARPZones.spinUpDelay
)
end
--
-- Start
--
function FARPZones.start()
-- check libs
if not dcsCommon.libCheck("cfx FARP Zones",
FARPZones.requiredLibs) then
return false
end
FARPZones.startingUp = true
-- install callbacks for FARP-relevant events
dcsCommon.addEventHandler(FARPZones.somethingHappened,
FARPZones.preProcessor,
FARPZones.postProcessor)
-- collect all FARP Zones
local theZones = cfxZones.getZonesWithAttributeNamed("FARP")
for k, aZone in pairs(theZones) do
local aFARP = FARPZones.createFARPFromZone(aZone) -- read attributes from DCS
FARPZones.addFARPZone(aFARP) -- add to managed zones
-- FARPZones.drawFarp(aFARP)
FARPZones.drawFARPCircleInMap(aFARP) -- mark in map
FARPZones.produceVehicles(aFARP) -- allocate initial vehicles
--trigger.action.outText("processed FARP " .. aZone.name .. " now owned by " .. aZone.owner, 30)
end
FARPZones.startingUp = false
trigger.action.outText("cf/x FARP Zones v" .. FARPZones.version .. " started", 30)
return true
end
-- let's get rolling
if not FARPZones.start() then
trigger.action.outText("cf/x FARP Zones aborted: missing libraries", 30)
FARPZones = nil
end
--[[--
Improvements:
per FARP/Helipad in zone: create resources (i.e. support multi 4-Pad FARPS out of the box
make hidden farps only appear for owning side
--]]--

203
modules/cargoSuper.lua Normal file
View File

@ -0,0 +1,203 @@
cargoSuper = {}
cargoSuper.version = "1.1.0"
--[[--
version history
1.0.0 - initial version
1.1.0 - removeMassObjectFrom supports name directly for mass object
- cargoSuper tracks all mass objects
- deleteMassObject()
- removeMassObjectFrom supports forget option
- createMassObject supports auto-gen UUID name
- removeAllMassForCargo renamed to removeAllMassForCategory
- category default "cSup!DefCat"
- getAllCategoriesFor alias for getAllCargos
- getManifestForCategory alias for getManifestFor
- removeAllMassFor()
CargoSuper manages weigth for a logical named unit. Weight can be added
to arbitrary categories like 'passengers', 'cargo' or "whatever". In order
to add weight to a unit, first create a massObject through createMassObject
and then add that mass object to the unit via addMassTo, with a category name
you can get access to massobjects via getMassObjects
When done, you can remove the mass object via removeMassObject or
removeAll
To get a unit's total weight, use getTotalMass()
IMPORTANT:
This module does ***N*O*T*** call trigger.action.setUnitInternalCargo
you must do that yourself
--]]--
cargoSuper.requiredLibs = {
"dcsCommon", -- common is of course needed for everything
-- pretty stupid to check for this since we
-- need common to invoke the check, but anyway
"nameStats", -- generic data module for weight
}
cargoSuper.cargos = {}
cargoSuper.massObjects = {}
-- create a massObject. reference object can be used to store
-- anything that links an associated object:getSampleRate()
-- massName can be anything but must be unique as it is used to store. pass nil for UUID-created name
function cargoSuper.createMassObject(massInKg, massName, referenceObject)
local theObject = {}
theObject.mass = massInKg
theObject.name = massName
theObject.ref = referenceObject
if not massName then
massName = dcsCommon.uuid("cSup!N")
end
local existingMO = cargoSuper.massObjects[massName]
if existingMO then
trigger.action.outText("+++cSuper: WARNING - " .. massName .. " exists already, overwritten!", 30)
end
cargoSuper.massObjects[massName] = theObject
return theObject
end
function cargoSuper.deleteMassObject(massObject)
if not massObject then return end
local theName = ""
if type(massObject) == "string" then
theName = massObject
else
theName = massObject.name
end
cargoSuper.massObjects[massName] = nil
end
function cargoSuper.addMassObjectTo(name, category, theMassObject)
if not theMassObject then return end
if not category then category = "cSup!DefCat" end
-- use nameStats to access private data table
local theMassTable = nameStats.getTable(name, category, cargoSuper.cargos)
theMassTable[theMassObject.name] = theMassObject
end
function cargoSuper.removeMassObjectFrom(name, category, theMassObject, forget)
if not theMassObject then return end
if not category then category = "cSup!DefCat" end
if not forget then forget = true end
-- use nameStats to access private data table
-- return the data table stored under category. category *can* be nil
-- v1.0.1 can also provide mass object name
-- instead of mass object itself. no check!!
local moName = ""
if type(theMassObject) == "string" then
moName = theMassObject
else
moName = theMassObject.name
end
local theMassTable = nameStats.getTable(name, category, cargoSuper.cargos)
theMassTable[moName] = nil
if forget then
cargoSuper.deleteMassObject(theMassObject)
end
end
-- DO NOT PUBLISH. Provided only for backwards compatibility
function cargoSuper.removeAllMassForCargo(name, catergory)
if not category then category = "cSup!DefCat" end
nameStats.reset(name, category, cargoSuper.cargos)
end
-- alias for removeAllMassForCargo
function cargoSuper.removeAllMassForCategory(name, catergory)
cargoSuper.removeAllMassForCargo(name, catergory)
end
function cargoSuper.removeAllMassFor(name)
if not name then return end
local categories = nameStats.getAllPathes(name, cargoSuper.cargos)
for idx, cat in pairs(categories) do
cargoSuper.removeAllMassForCategory(name, cat)
end
end
-- returns all cargo categories for name
-- DO NOT PUBLISH. BAD NAMING
function cargoSuper.getAllCargosFor(name)
local categories = nameStats.getAllPathes(name, cargoSuper.cargos)
return categories
end
-- alias for badly named method above
function cargoSuper.getAllCategoriesFor(name)
cargoSuper.getAllCargosFor(name)
end
-- return all mass objects that are in name, category as table
-- that can be accessed as *array*
-- DO NOT PUBLISH. NAMING IS BAD
function cargoSuper.getManifestFor(name, category)
if not category then category = "cSup!DefCat" end
local theMassTable = nameStats.getTable(name, category, cargoSuper.cargos)
return dcsCommon.enumerateTable(theMassTable)
end
-- alias for badly named method above
function cargoSuper.getManifestForCategory(name, category)
cargoSuper.getManifestFor(name, category)
end
function getManifestTextFor(name, category, includeTotal)
if not category then category = "cSup!DefCat" end
local theMassTable = cargoSuper.getManifestFor(name, category)
local desc = ""
local totalMass = 0
local isFirst = true
for idx, massObject in pairs(theMassTable) do
if not isFirst then
desc = desc .. "\n"
end
totalMass = totalMass + massObject.mass
desc = desc .. massObject.name .. " (" .. massObject.mass .. "kg)"
isFirst = false
end
if includeTotal and (isFirst == false) then
-- we only do this if we have at least one (isFirst is false)
desc = desc .. "\nTotal Weight: " .. totalMass .. "kg"
end
return desc
end
function cargoSuper.calculateTotalMassForCategory(name, category)
if not category then category = "cSup!DefCat" end
theMasses = cargoSuper.getManifestFor(name, category)
local totalMass = 0
for massName, massObject in pairs(theMasses) do
totalMass = totalMass + massObject.mass
end
return totalMass
end
function cargoSuper.calculateTotalMassFor(name)
local allCategories = cargoSuper.getAllCargosFor(name)
local totalMass = 0
for idx, category in pairs(allCategories) do
totalMass = totalMass + cargoSuper.calculateTotalMassForCategory(name, category)
end
return totalMass
end
function cargoSuper.start()
-- make sure we have loaded all relevant libraries
if not dcsCommon.libCheck("cfx CargoSuper", cargoSuper.requiredLibs) then
trigger.action.outText("cf/x CargoSuper aborted: missing libraries", 30)
return false
end
trigger.action.outText("cf/x CargoSuper v" .. cargoSuper.version .. " loaded", 30)
return true
end
-- go go go
if not cargoSuper.start() then
cargoSuper = nil
end

406
modules/cfxArtillery.lua Normal file
View File

@ -0,0 +1,406 @@
cfxArtilleryDemon = {}
cfxArtilleryDemon.version = "1.0.2"
-- based on cfx stage demon v 1.0.2
cfxArtilleryDemon.messageToAll = true -- set to false if messages should be sent only to the group that set the mark
cfxArtilleryDemon.messageTime = 30 -- how long a message stays on the sceeen
-- cfxArtillery hooks into DCS's mark system to intercept user
-- transactions with the mark system and uses that for arty targeting
-- used to interactively add ArtilleryZones during gameplay
-- Copyright (c) 2021 by Christian Franz and cf/x AG
cfxArtilleryDemon.autostart = true -- start automatically
-- whenever you begin a Mark with the string below, it will be taken as a command
-- and run through the command parser, stripping the mark, and then splitting
-- by blanks
cfxArtilleryDemon.markOfDemon = "-" -- all commands must start with this sequence
cfxArtilleryDemon.splitDelimiter = " "
cfxArtilleryDemon.unitFilterMethod = nil -- optional user filtering redirection. currently
-- set to allow all users use cfxArtillery
cfxArtilleryDemon.processCommandMethod = nil -- optional initial command processing redirection
-- currently set to cfxArtillery's own processor
cfxArtilleryDemon.commandTable = {} -- key, value pair for command processing per keyword
-- all commands cfxArtillery understands are used as keys and
-- the functions that process them are used as values
-- making the parser a trivial table :)
cfxArtilleryDemon.demonID = nil -- used only for suspending the event callback
-- unit authorization. You return false to disallow this unit access
-- to commands
-- simple authorization checks would be to allow only players
-- on neutral side, or players in range of location with Lino of sight
-- to that point
--
function cfxArtilleryDemon.authorizeAllUnits(event)
-- units/groups that are allowed to give a command can be filtered.
-- return true if the unit/group may give commands
-- cfxArtillery allows anyone to give it commands
return true
end
function cfxArtilleryDemon.hasMark(theString)
-- check if the string begins with the sequece to identify commands
if not theString then return false end
return theString:find(cfxArtilleryDemon.markOfDemon) == 1
end
function cfxArtilleryDemon.splitString(inputstr, sep)
if sep == nil then
sep = "%s"
end
if inputstr == nil then
inputstr = ""
end
local t={}
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
table.insert(t, str)
end
return t
end
function cfxArtilleryDemon.str2num(inVal, default)
if not default then default = 0 end
if not inVal then return default end
if type(inVal) == "number" then return inVal end
local num = nil
if type(inVal) == "string" then num = tonumber(inVal) end
if not num then return default end
return num
end
--
-- output method. can be customized, so we have a central place where we
-- can control how output is handled. Is currently outText and outTextToGroup
--
function cfxArtilleryDemon.outMessage(theMessage, args)
if not args then
args = {}
end
local toAll = args.toAll -- will only be true if defined and set to true
if not args.group then
toAll = true
else
if not args.group:isExist() then
toAll = true
end
end
toAll = toAll or cfxArtilleryDemon.messageToAll
if not toAll then
trigger.action.outTextToGroup(args.group, theMessage, cfxArtilleryDemon.messageTime)
else
trigger.action.outText(theMessage, cfxArtilleryDemon.messageTime)
end
end
--
-- get all player groups - since there is no getGroupByIndex in DCS (yet)
-- we simply collect all player groups (since only palyers can place marks)
-- and try to match their group ID to the one given by mark
function cfxArtilleryDemon.getAllPayerGroups()
local coalitionSides = {0, 1, 2} -- we currently have neutral, red, blue
local playerGroups = {}
for i=1, #coalitionSides do
local theSide = coalitionSides[i]
-- get all players for this side
local thePlayers = coalition.getPlayers(theSide)
for p=1, #thePlayers do
aPlayerUnit = thePlayers[p] -- docs say this is a unit table, not a person!
if aPlayerUnit:isExist() then
local theGroup = aPlayerUnit:getGroup()
if theGroup:isExist() then
local gID = theGroup:getID()
playerGroups[gID] = theGroup -- multiple players per group results in one group
end
end
end
end
return playerGroups
end
function cfxArtilleryDemon.retrieveGroupFromEvent(theEvent)
-- DEBUG CODE
if theEvent.initiator then
trigger.action.outText("EVENT: initiator set to " .. theEvent.initiator:getName(), 30)
else
trigger.action.outText("EVENT: NO INITIATOR", 30)
end
trigger.action.outText("EVENT: groupID = " .. theEvent.groupID, 30)
-- trivial case: initiator is set, and we can access the group
if theEvent.initiator then
if theEvent.initiator:isExist() then
return theEvent.initiator:getGroup()
end
end
-- ok, bad news: initiator wasn't filled. let's try the fallback: event.groupID
if theEvent.groupID and theEvent.groupID > 0 then
local playerGroups = cfxArtilleryDemon.getAllPayerGroups()
if playerGroups[theEvent.groupID] then
return palyerGroups[theEvent.groupID]
end
end
-- nope, return nil
return nil
end
-- main hook into DCS. Called whenever a Mark-related event happens
-- very simple: look if text begins with special sequence, and if so,
-- call the command processor. Note that you can hook your own command
-- processor in by changing the value of processCommandMethod
function cfxArtilleryDemon:onEvent(theEvent)
-- while we can hook into any of the three events,
-- we curently only utilize CHANGE Mark
if not (theEvent.id == world.event.S_EVENT_MARK_ADDED) and
not (theEvent.id == world.event.S_EVENT_MARK_CHANGE) and
not (theEvent.id == world.event.S_EVENT_MARK_REMOVED) then
-- not of interest for us, bye bye
return
end
-- build the messageOut() arg table
local args = {}
args.toAll = cfxArtilleryDemon.toAll
--
--
args.toAll = false -- FORCE GROUPS FOR DEBUGGING OF NEW CODE
--
--
if not args.toAll then
-- we want group-targeted messaging
-- so we need to retrieve the group
local theGroup = cfxArtilleryDemon.retrieveGroupFromEvent(theEvent)
if not theGroup then
args.toAll = true
trigger.action.messageOut("*** WARNING: cfxArtilleryDemon can't find group for command", 30)
else
args.group = theGroup
end
end
cfxArtilleryDemon.args = args -- copy reference so we can easily use it in messageOut
-- when we get here, we have a mark event
-- see if the unit filter lets it pass
if not cfxArtilleryDemon.unitFilterMethod(theEvent) then
return -- unit is not allowed to give demon orders. bye bye
end
if theEvent.id == world.event.S_EVENT_MARK_ADDED then
-- add mark is quite useless for us as we are called when the user clicks, with no
-- text in the description yet. Later abilities may want to use it though
end
if theEvent.id == world.event.S_EVENT_MARK_CHANGE then
-- when changed, the mark's text is examined for a command
-- if it starts with the 'mark' string ("*" by default) it is processed
-- by the command processor
-- if it is processed succesfully, the mark is immediately removed
-- else an error is displayed and the mark remains.
if cfxArtilleryDemon.hasMark(theEvent.text) then
-- strip the mark
local commandString = theEvent.text:sub(1+cfxArtilleryDemon.markOfDemon:len())
-- break remainder apart into <command> <arg1> ... <argn>
local commands = cfxArtilleryDemon.splitString(commandString, cfxArtilleryDemon.splitDelimiter)
-- this is a command. process it and then remove it if it was executed successfully
local success = cfxArtilleryDemon.processCommandMethod(commands, theEvent)
-- remove this mark after successful execution
if success then
trigger.action.removeMark(theEvent.idx)
cfxArtilleryDemon.outMessage("executed command <" .. commandString .. "> from unit" .. theEvent.initiator:getName(), args)
else
-- we could play some error sound
end
end
end
if theEvent.id == world.event.S_EVENT_MARK_REMOVED then
end
end
--
-- add / remove commands to/from cfxArtillerys vocabulary
--
function cfxArtilleryDemon.addCommndProcessor(command, processor)
cfxArtilleryDemon.commandTable[command:upper()] = processor
end
function cfxArtilleryDemon.removeCommandProcessor(command)
cfxArtilleryDemon.commandTable[command:upper()] = nil
end
--
-- process input arguments. Here we simply move them
-- up by one.
--
function cfxArtilleryDemon.getArgs(theCommands)
local args = {}
for i=2, #theCommands do
table.insert(args, theCommands[i])
end
return args
end
--
-- stage demon's main command interpreter.
-- magic lies in using the keywords as keys into a
-- function table that holds all processing functions
-- I wish we had that back in the Oberon days.
--
function cfxArtilleryDemon.executeCommand(theCommands, event)
-- trigger.action.outText("executor: *" .. theCommands[1] .. "*", 30)
-- see if theCommands[1] exists in the command table
local cmd = theCommands[1]
local arguments = cfxArtilleryDemon.getArgs(theCommands)
if not cmd then return false end
-- use the command as index into the table of functions
-- that handle them.
if cfxArtilleryDemon.commandTable[cmd:upper()] then
local theInvoker = cfxArtilleryDemon.commandTable[cmd:upper()]
local success = theInvoker(arguments, event)
return success
else
trigger.action.outText("***error: unknown command <".. cmd .. ">", 30)
return false
end
return true
end
--
-- SMOKE COMMAND
--
-- known commands and their processors
function cfxArtilleryDemon.smokeColor2Index (theColor)
local color = theColor:lower()
if color == "red" then return 1 end
if color == "white" then return 2 end
if color == "orange" then return 3 end
if color == "blue" then return 4 end
return 0
end
-- this is the command processing template for your own commands
-- when you add a command processor via addCommndProcessor()
-- smoke command syntax: '-smoke <color>' with optional color, color being red, green, blue, white or orange
function cfxArtilleryDemon.processSmokeCommand(args, event)
if not args[1] then args[1] = "red" end -- default to red color
local thePoint = event.pos
thePoint.y = land.getHeight({x = thePoint.x, y = thePoint.z}) +3 -- elevate to ground height
trigger.action.smoke(thePoint, cfxArtilleryDemon.smokeColor2Index(args[1]))
return true
end
--
-- BOOM command
--
function cfxArtilleryDemon.doBoom(args)
--trigger.action.outText("sim shell str=" .. args.strength .. " x=" .. args.point.x .. " z = " .. args.point.z .. " Tdelta = " .. args.tDelta, 30)
-- trigger.action.smoke(args.point, 2)
trigger.action.explosion(args.point, args.strength)
end
function cfxArtilleryDemon.processBoomCommand(args, event)
if not args[1] then args[1] = "750" end -- default to 750 strength
local transitionTime = 20 -- seconds until shells hit
local shellNum = 17
local shellBaseStrength = 500
local shellvariance = 0.2 -- 10%
local center = event.pos -- center of where shells hit
center.y = land.getHeight({x = center.x, y = center.z}) + 3
-- we now can 'dirty' the position by something. not yet
for i=1, shellNum do
local thePoint = dcsCommon.randomPointInCircle(100, 0, center.x, center.z)
local boomArgs = {}
local strVar = shellBaseStrength * shellvariance
strVar = strVar * (2 * dcsCommon.randomPercent() - 1.0) -- go from -1 to 1
boomArgs.strength = shellBaseStrength + strVar
thePoint.y = land.getHeight({x = thePoint.x, y = thePoint.z}) + 1 -- elevate to ground height + 1
boomArgs.point = thePoint
local timeVar = 5 * (2 * dcsCommon.randomPercent() - 1.0) -- +/- 1.5 seconds
boomArgs.tDelta = timeVar
timer.scheduleFunction(cfxArtilleryDemon.doBoom, boomArgs, timer.getTime() + transitionTime + timeVar)
end
trigger.action.outText("Fire command confirmed. Artillery is firing at your designated co-ordinates.", 30)
trigger.action.smoke(center, 2) -- mark location visually
return true
end
--
-- cfxArtilleryZones interface
--
function cfxArtilleryDemon.processTargetCommand(args, event)
-- get position
local center = event.pos -- center of where shells hit
center.y = land.getHeight({x = center.x, y = center.z})
if not event.initiator then
trigger.action.outText("Target entry aborted: no initiator.", 30)
return true
end
local theUnit = event.initiator
local theGroup = theUnit:getGroup()
local coalition = theGroup:getCoalition()
local spotRange = 3000
local autoAdd = true
local params = ""
for idx, param in pairs(args) do
if params == "" then params = ": "
else params = params .. " "
end
params = params .. param
end
local name = "TgtData".. params .. " (" .. theUnit:getName() .. ")@T+" .. math.floor(timer.getTime())
-- feed into arty zones
cfxArtilleryZones.createArtilleryZone(name, center, coalition, spotRange, 500, autoAdd) -- 500 is base strength
trigger.action.outTextForCoalition(coalition, "New ARTY coordinates received from " .. theUnit:getName() .. ", standing by", 30)
return true
end
--
-- cfxArtillery init and start
--
function cfxArtilleryDemon.init()
cfxArtilleryDemon.unitFilterMethod = cfxArtilleryDemon.authorizeAllUnits
cfxArtilleryDemon.processCommandMethod = cfxArtilleryDemon.executeCommand
-- now add known commands to interpreter. Add your own commands the same way
cfxArtilleryDemon.addCommndProcessor("smoke", cfxArtilleryDemon.processSmokeCommand)
cfxArtilleryDemon.addCommndProcessor("bumm", cfxArtilleryDemon.processBoomCommand)
cfxArtilleryDemon.addCommndProcessor("tgt",
cfxArtilleryDemon.processTargetCommand)
-- you can add and remove command the same way
trigger.action.outText("cf/x cfx Artillery Demon v" .. cfxArtilleryDemon.version .. " loaded", 30)
end
function cfxArtilleryDemon.start()
cfxArtilleryDemon.demonID = world.addEventHandler(cfxArtilleryDemon)
trigger.action.outText("cf/x cfxArtilleryDemon v" .. cfxArtilleryDemon.version .. " started", 30)
end
cfxArtilleryDemon.init()
if cfxArtilleryDemon.autostart then
cfxArtilleryDemon.start()
end

714
modules/cfxArtilleryUI.lua Normal file
View File

@ -0,0 +1,714 @@
cfxArtilleryUI = {}
cfxArtilleryUI.version = "1.1.0"
cfxArtilleryUI.requiredLibs = {
"dcsCommon", -- always
"cfxPlayer", -- get all players
"cfxZones", -- Zones, of course
"cfxArtilleryZones", -- this is where we get zones from
}
--
-- UI for ArtilleryZones module, implements LOS, Observer, SMOKE
-- Copyright (c) 2021, 2022 by Christian Franz and cf/x AG
--[[-- VERSION HISTORY
- 1.0.0 - based on jtacGrpUI
- 1.0.1 - tgtCheckSum
- smokeCheckSum
- ability to smoke target zone in range
- 1.1.0 - config zone
- allowPlanes flag
- config smoke color
- collect zones recognizes moving zones, updates landHeight
- allSeeing god mode attribute: always observing.
- allRanging god mode attribute: always in range.
--]]--
cfxArtilleryUI.allowPlanes = false -- if false, heli only
cfxArtilleryUI.smokeColor = "red" -- for smoking target zone
-- find artiller zones, command fire
cfxArtilleryUI.updateDelay = 1 -- seconds until we update target list
cfxArtilleryUI.groupConfig = {} -- all inited group private config data
cfxArtilleryUI.updateSound = "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav"
cfxArtilleryUI.maxSmokeDist = 30000 -- in meters. Distance to target to populate smoke menu
--
-- C O N F I G H A N D L I N G
-- =============================
--
-- Each group has their own config block that can be used to
-- store group-private data and configuration items.
--
function cfxArtilleryUI.resetConfig(conf)
conf.tgtCheckSum = nil -- used to determine if we need to update target menu
conf.smokeCheckSum = nil -- used to determine if we need to update smoke menu
end
function cfxArtilleryUI.createDefaultConfig(theGroup)
local conf = {}
conf.theGroup = theGroup
conf.name = theGroup:getName()
conf.id = theGroup:getID()
conf.coalition = theGroup:getCoalition()
cfxArtilleryUI.resetConfig(conf)
conf.mainMenu = nil; -- this is where we store the main menu if we branch
conf.myCommands = nil; -- this is where we store the commands if we branch
return conf
end
-- getConfigFor group will allocate if doesn't exist in DB
-- and add to it
function cfxArtilleryUI.getConfigForGroup(theGroup)
if not theGroup then
trigger.action.outText("+++WARNING: cfxArtilleryUI nil group in getConfigForGroup!", 30)
return nil
end
local theName = theGroup:getName()
local c = cfxArtilleryUI.getConfigByGroupName(theName) -- we use central accessor
if not c then
c = cfxArtilleryUI.createDefaultConfig(theGroup)
cfxArtilleryUI.groupConfig[theName] = c -- should use central accessor...
end
return c
end
function cfxArtilleryUI.getConfigByGroupName(theName) -- DOES NOT allocate when not exist
if not theName then return nil end
return cfxArtilleryUI.groupConfig[theName]
end
function cfxArtilleryUI.getConfigForUnit(theUnit)
-- simple one-off step by accessing the group
if not theUnit then
trigger.action.outText("+++WARNING: cfxArtilleryUI nil unit in getConfigForUnit!", 30)
return nil
end
local theGroup = theUnit:getGroup()
return getConfigForGroup(theGroup)
end
--
--
-- M E N U H A N D L I N G
-- =========================
--
--
function cfxArtilleryUI.clearCommsTargets(conf)
if conf.myTargets then
for i=1, #conf.myTargets do
missionCommands.removeItemForGroup(conf.id, conf.myTargets[i])
end
end
conf.myTargets={}
conf.tgtCheckSum = nil
end
function cfxArtilleryUI.clearCommsSmokes(conf)
if conf.mySmokes then
for i=1, #conf.mySmokes do
missionCommands.removeItemForGroup(conf.id, conf.mySmokes[i])
end
end
conf.mySmokes={}
conf.smokeCheckSum = nil
end
function cfxArtilleryUI.clearCommsSubmenus(conf)
if conf.myCommands then
for i=1, #conf.myCommands do
missionCommands.removeItemForGroup(conf.id, conf.myCommands[i])
end
end
conf.myCommands = {}
-- now clear target menu
cfxArtilleryUI.clearCommsTargets(conf)
if conf.myTargetMenu then
missionCommands.removeItemForGroup(conf.id, conf.myTargetMenu)
conf.myTargetMenu = nil
end
-- now clear smoke menu
cfxArtilleryUI.clearCommsSmokes(conf)
if conf.mySmokeMenu then
missionCommands.removeItemForGroup(conf.id, conf.mySmokeMenu)
conf.mySmokeMenu = nil
end
end
function cfxArtilleryUI.removeCommsFromConfig(conf)
cfxArtilleryUI.clearCommsSubmenus(conf)
if conf.myMainMenu then
missionCommands.removeItemForGroup(conf.id, conf.myMainMenu)
conf.myMainMenu = nil
end
end
-- this only works in single-unit groups. may want to check if group
-- has disappeared
function cfxArtilleryUI.removeCommsForUnit(theUnit)
if not theUnit then return end
if not theUnit:isExist() then return end
-- perhaps add code: check if group is empty
local conf = cfxArtilleryUI.getConfigForUnit(theUnit)
cfxArtilleryUI.removeCommsFromConfig(conf)
end
function cfxArtilleryUI.removeCommsForGroup(theGroup)
if not theGroup then return end
if not theGroup:isExist() then return end
local conf = cfxArtilleryUI.getConfigForGroup(theGroup)
cfxArtilleryUI.removeCommsFromConfig(conf)
end
--
-- set main root in F10 Other. All sub menus click into this
--
function cfxArtilleryUI.isEligibleForMenu(theGroup)
if cfxArtilleryUI.allowPlanes then return true end
-- only allow helicopters for Forward Observervation
local cat = theGroup:getCategory()
if cat ~= Group.Category.HELICOPTER then return false end
return true
end
function cfxArtilleryUI.setCommsMenuForUnit(theUnit)
if not theUnit then
trigger.action.outText("+++WARNING: cfxArtilleryUI nil UNIT in setCommsMenuForUnit!", 30)
return
end
if not theUnit:isExist() then return end
local theGroup = theUnit:getGroup()
cfxArtilleryUI.setCommsMenu(theGroup)
end
function cfxArtilleryUI.setCommsMenu(theGroup)
-- depending on own load state, we set the command structure
-- it begins at 10-other, and has 'jtac' as main menu with submenus
-- as required
if not theGroup then return end
if not theGroup:isExist() then return end
-- we test here if this group qualifies for
-- the menu. if not, exit
if not cfxArtilleryUI.isEligibleForMenu(theGroup) then return end
local conf = cfxArtilleryUI.getConfigForGroup(theGroup)
conf.id = theGroup:getID(); -- we do this ALWAYS so it is current even after a crash
-- ok, first, if we don't have an F-10 menu, create one
if not (conf.myMainMenu) then
conf.myMainMenu = missionCommands.addSubMenuForGroup(conf.id, 'Forward Observer')
end
-- clear out existing commands
cfxArtilleryUI.clearCommsSubmenus(conf)
-- now we have a menu without submenus.
-- add our own submenus
cfxArtilleryUI.addSubMenus(conf)
end
function cfxArtilleryUI.addSubMenus(conf)
-- add menu items to choose from after
-- user clickedf on MAIN MENU. In this implementation
-- they all result invoked methods
local commandTxt = "List Artillery Targets"
local theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myMainMenu,
cfxArtilleryUI.redirectCommandListTargets,
{conf, "arty list"}
)
table.insert(conf.myCommands, theCommand)
-- add a targets menu. this will be regularly set (every x seconds)
conf.myTargetMenu = missionCommands.addSubMenuForGroup(conf.id, 'Artillery Fire Control', conf.myMainMenu)
-- populate this target menu with commands
-- creates a tgtCheckSum to very each time
cfxArtilleryUI.populateTargetMenu(conf)
conf.mySmokeMenu = missionCommands.addSubMenuForGroup(conf.id, 'Mark Artillery Target', conf.myMainMenu)
cfxArtilleryUI.populateSmokeMenu(conf)
end
function cfxArtilleryUI.populateTargetMenu(conf)
local targetList = cfxArtilleryUI.collectArtyTargets(conf)
-- iterate the list
-- we use a control string to know if we have to change
local tgtCheckSum = ""
-- now filter target list
local filteredTargets = {}
for idx, aTarget in pairs(targetList) do
local inRange = cfxArtilleryUI.allRanging or aTarget.range * 1000 < aTarget.spotRange
if inRange then
isVisible = cfxArtilleryUI.allSeeing or land.isVisible(aTarget.here, aTarget.there)
if isVisible then
table.insert(filteredTargets, aTarget)
tgtCheckSum = tgtCheckSum .. aTarget.name
end
end
end
-- now compare old control string with new, and only
-- re-populate if the old is different
if tgtCheckSum == conf.tgtCheckSum then
-- trigger.action.outText("*** yeah old targets", 30)
return
elseif not conf.tgtCheckSum then
-- trigger.action.outText("+++ new target menu", 30)
else
trigger.action.outTextForGroup(conf.id, "Artillery target updates", 30)
trigger.action.outSoundForGroup(conf.id, cfxArtilleryUI.updateSound)
-- trigger.action.outText("!!! target update ", 30)
end
-- we need to re-populate. erase old values
cfxArtilleryUI.clearCommsTargets(conf)
conf.tgtCheckSum = tgtCheckSum -- remember for last time
--trigger.action.outText("new targets", 30)
if #filteredTargets < 1 then
-- simply put one-line dummy in there
local commandTxt = "(No unobscured target areas)"
local theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myTargetMenu,
cfxArtilleryUI.dummyCommand,
{conf, "nix is"}
)
table.insert(conf.myTargets, theCommand)
return
end
-- now populate target menu, max 8 items
local numTargets = #filteredTargets
if numTargets > 8 then numTargets = 8 end
for i=1, numTargets do
-- make a target command for each
local aTarget = filteredTargets[i]
commandTxt = "Fire at: <" .. aTarget.name .. ">"
theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myTargetMenu,
cfxArtilleryUI.redirectFireCommand,
{conf, aTarget}
)
table.insert(conf.myTargets, theCommand)
end
end
function cfxArtilleryUI.populateSmokeMenu(conf)
local targetList = cfxArtilleryUI.collectArtyTargets(conf) -- we can use target gathering
-- now iterate the reulting list
-- we use a control string to know if we have to change
local smokeCheckSum = ""
-- now filter target list
local filteredTargets = {}
for idx, aTarget in pairs(targetList) do
local inRange = cfxArtilleryUI.allRanging or aTarget.range * 1000 < cfxArtilleryUI.maxSmokeDist
if inRange then
table.insert(filteredTargets, aTarget)
smokeCheckSum = smokeCheckSum .. aTarget.name
end
end
-- now compare old control string with new, and only
-- re-populate if the old is different
if smokeCheckSum == conf.smokeCheckSum then
-- nothing changed since last time, do nothing and return immediately
return
end
-- we need to re-populate. erase old values
cfxArtilleryUI.clearCommsSmokes(conf)
conf.smokeCheckSum = smokeCheckSum -- remember for last time
if #filteredTargets < 1 then
-- simply put one-line dummy in there
local commandTxt = "(No targets in range)"
local theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.mySmokeMenu,
cfxArtilleryUI.dummyCommand, -- the "nix is" command
{conf, "nix is"}
)
table.insert(conf.mySmokes, theCommand)
return
end
-- now populate target menu, max 8 items
local numTargets = #filteredTargets
if numTargets > 10 then numTargets = 10 end
for i=1, numTargets do
-- make a target command for each
local aTarget = filteredTargets[i]
commandTxt = "Smoke <" .. aTarget.name .. ">"
theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.mySmokeMenu,
cfxArtilleryUI.redirectSmokeCommand,
{conf, aTarget}
)
table.insert(conf.mySmokes, theCommand)
end
end
--
-- each menu item has a redirect and timed invoke to divorce from the
-- no-debug zone in the menu invocation. Delay is .1 seconds
--
function cfxArtilleryUI.dummyCommand(args)
-- do nothing, dummy!
end
function cfxArtilleryUI.redirectFireCommand(args)
timer.scheduleFunction(cfxArtilleryUI.doFireCommand, args, timer.getTime() + 0.1)
end
function cfxArtilleryUI.doFireCommand(args)
local conf = args[1] -- < conf in here
local aTarget = args[2] -- < second argument in here
local theGroup = conf.theGroup
local now = timer.getTime()
-- recalc range since it may be 10 seconds old
local here = dcsCommon.getGroupLocation(theGroup)
local there = aTarget.there
local lRange = dcsCommon.dist(here, there)
aTarget.range = lRange/1000
aTarget.range = math.floor(aTarget.range * 10) / 10
local inTime = cfxArtilleryUI.allTiming or now > aTarget.zone.artyCooldownTimer
if inTime then
-- invoke fire command for artyZone
trigger.action.outTextForGroup(conf.id, "Roger, " .. theGroup:getName() .. ", firing at " .. aTarget.name, 30)
if cfxArtilleryUI.allRanging then lRange = 1 end -- max accuracy
cfxArtilleryZones.simFireAtZone(aTarget.zone, theGroup, lRange)
else
-- way want to stay silent and simply iterate?
trigger.action.outTextForGroup(conf.id, "Artillery is reloading", 30)
end
end
--
-- SMOKE EM
--
function cfxArtilleryUI.redirectSmokeCommand(args)
timer.scheduleFunction(cfxArtilleryUI.doSmokeCommand, args, timer.getTime() + 0.1)
end
function cfxArtilleryUI.doSmokeCommand(args)
local conf = args[1] -- < conf in here
local aTarget = args[2] -- < second argument in here
local theGroup = conf.theGroup
-- invoke smoke command for artyZone
trigger.action.outTextForGroup(conf.id, "Roger, " .. theGroup:getName() .. ", marking " .. aTarget.name, 30)
cfxArtilleryZones.simSmokeZone(aTarget.zone, theGroup, cfxArtilleryUI.smokeColor)
end
--
--
--
function cfxArtilleryUI.redirectCommandListTargets(args)
timer.scheduleFunction(cfxArtilleryUI.doCommandListTargets, args, timer.getTime() + 0.1)
end
function cfxArtilleryUI.doCommandListTargets(args)
local conf = args[1] -- < conf in here
local what = args[2] -- < second argument in here
local theGroup = conf.theGroup
-- trigger.action.outTextForGroup(conf.id, "+++ groupUI: processing comms menu for <" .. what .. ">", 30)
local targetList = cfxArtilleryUI.collectArtyTargets(conf)
-- iterate the list
if #targetList < 1 then
trigger.action.outTextForGroup(conf.id, "\nArtillery Targets:\nNo active artillery targets\n", 30)
return
end
local desc = "Artillery Targets:\n"
-- trigger.action.outTextForGroup(conf.id, "Target Report:", 30)
for i=1, #targetList do
local aTarget = targetList[i]
local inRange = cfxArtilleryUI.allRanging or aTarget.range * 1000 < aTarget.spotRange
if inRange then
isVisible = cfxArtilleryUI.allSeeing or land.isVisible(aTarget.here, aTarget.there)
if isVisible then
desc = desc .. "\n" .. aTarget.name .. " - OBSERVING"
else
desc = desc .. "\n" .. aTarget.name .. " - TARGET OBSCURED"
end
else
desc = desc .. "\n" .. aTarget.name .. " [" .. aTarget.range .. "km at " .. aTarget.bearing .. "°]"
end
end
trigger.action.outTextForGroup(conf.id, desc .. "\n", 30, true)
end
function cfxArtilleryUI.collectArtyTargets(conf)
-- iterate all target zones, for those that are on my side
-- calculate range, bearing, and then order by distance
local theTargets = {}
for idx, aZone in pairs(cfxArtilleryZones.artilleryZones) do
if aZone.coalition == conf.coalition then
table.insert(theTargets, aZone)
end
end
-- we now have a list of all Arty target Zones
-- get bearing and range to targets, and sort them accordingly
-- WARNING: aTarget.range is in KILOmeters!
local targetList = {}
local here = dcsCommon.getGroupLocation(conf.theGroup) -- this is me
for idx, aZone in pairs (theTargets) do
local aTarget = {}
-- establish our location
aTarget.zone = aZone
aTarget.name = aZone.name
aTarget.here = here
--aTarget.targetName = aZone.name
aTarget.spotRange = aZone.spotRange
-- get the target we are lazing
local zP = cfxZones.getPoint(aZone) -- zone can move!
aZone.landHeight = land.getHeight({x = zP.x, y= zP.z})
local there = {x = zP.x, y = aZone.landHeight + 1, z=zP.z}
aTarget.there = there
aTarget.range = dcsCommon.dist(here, there) / 1000 -- (in km)
aTarget.range = math.floor(aTarget.range * 10) / 10
aTarget.bearing = dcsCommon.bearingInDegreesFromAtoB(here, there)
--aTarget.jtacName = troop.name
table.insert(targetList, aTarget)
end
-- now sort by range
table.sort(targetList, function (left, right) return left.range < right.range end )
-- return list sorted by distance
return targetList
end
function cfxArtilleryUI.redirectCommandFire(args)
timer.scheduleFunction(cfxArtilleryUI.doCommandFire, args, timer.getTime() + 0.1)
end
function cfxArtilleryUI.doCommandFire(args)
local conf = args[1] -- < conf in here
local what = args[2] -- < second argument in here
local theGroup = conf.theGroup
-- sort all arty groups by distance
local targetList = cfxArtilleryUI.collectArtyTargets(conf, true)
if #targetList < 1 then
trigger.action.outTextForGroup(conf.id, "You are currently not observing a target zone. Move closer.", 30)
return
end
local now = timer.getTime()
-- for all that we are in range, that are visible and that can fire
for idx, aTarget in pairs(targetList) do
local inRange = (aTarget.range * 1000 < aTarget.spotRange) or cfxArtilleryUI.allRanging
local inTime = cfxArtilleryUI.allTiming or now > aTarget.zone.artyCooldownTimer
if inRange then
isVisible = cfxArtilleryUI.allSeeing or land.isVisible(aTarget.here, aTarget.there)
if isVisible then
if inTime then
-- invoke fire command for artyZone
cfxArtilleryZones.simFireAtZone(aTarget.zone, theGroup, aTarget.range)
-- return -- we only fire one zone
else
-- way want to stay silent and simply iterate?
trigger.action.outTextForGroup(conf.id, "Artillery reloading", 30)
end
end
return -- after the first in range we stop, no matter what
else
-- not interesting
end
end
-- issue a fire command
end
--
-- G R O U P M A N A G E M E N T
--
-- Group Management is required to make sure all groups
-- receive a comms menu and that they receive a clean-up
-- when required
--
-- Callbacks are provided by cfxPlayer module to which we
-- subscribe during init
--
function cfxArtilleryUI.playerChangeEvent(evType, description, player, data)
--trigger.action.outText("+++ groupUI: received <".. evType .. "> Event", 30)
if evType == "newGroup" then
-- initialized attributes are in data as follows
-- .group - new group
-- .name - new group's name
-- .primeUnit - the unit that trigggered new group appearing
-- .primeUnitName - name of prime unit
-- .id group ID
--theUnit = data.primeUnit
cfxArtilleryUI.setCommsMenu(data.group)
return
end
if evType == "removeGroup" then
-- data is the player record that no longer exists. it consists of
-- .name
-- we must remove the comms menu for this group else we try to add another one to this group later
local conf = cfxArtilleryUI.getConfigByGroupName(data.name)
if conf then
cfxArtilleryUI.removeCommsFromConfig(conf) -- remove menus
cfxArtilleryUI.resetConfig(conf) -- re-init this group for when it re-appears
else
trigger.action.outText("+++ jtacUI: can't retrieve group <" .. data.name .. "> config: not found!", 30)
end
return
end
if evType == "leave" then
-- player unit left.
end
if evType == "unit" then
-- player changed units.
end
end
--
-- update
--
function cfxArtilleryUI.updateGroup(theGroup)
if not theGroup then return end
if not theGroup:isExist() then return end
-- we test here if this group qualifies for
-- the menu. if not, exit
if not cfxArtilleryUI.isEligibleForMenu(theGroup) then return end
local conf = cfxArtilleryUI.getConfigForGroup(theGroup)
conf.id = theGroup:getID(); -- we do this ALWAYS
-- populateTargetMenu erases old settings by itself
cfxArtilleryUI.populateTargetMenu(conf) -- update targets
cfxArtilleryUI.populateSmokeMenu(conf) -- update targets
end
function cfxArtilleryUI.update()
-- reschedule myself in x seconds
timer.scheduleFunction(cfxArtilleryUI.update, {}, timer.getTime() + cfxArtilleryUI.updateDelay)
-- iterate all groups, and rebuild their target menus
local allPlayerGroups = cfxPlayerGroups -- cfxPlayerGroups is a global, don't fuck with it!
-- contains per group player record. Does not resolve on unit level!
for gname, pgroup in pairs(allPlayerGroups) do
local theUnit = pgroup.primeUnit -- single unit groups!
local theGroup = theUnit:getGroup()
cfxArtilleryUI.updateGroup(theGroup)
end
end
--
-- Config Zone
--
function cfxArtilleryUI.readConfigZone()
-- note: must match exactly!!!!
local theZone = cfxZones.getZoneByName("ArtilleryUIConfig")
if not theZone then
trigger.action.outText("+++A-UI: no config zone!", 30)
return
end
trigger.action.outText("+++A-UI: found config zone!", 30)
cfxArtilleryUI.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
cfxArtilleryUI.allowPlanes = cfxZones.getBoolFromZoneProperty(theZone, "allowPlanes", false)
cfxArtilleryUI.smokeColor = cfxZones.getSmokeColorStringFromZoneProperty(theZone, "smokeColor", "red")
cfxArtilleryUI.allSeeing = cfxZones.getBoolFromZoneProperty(theZone, "allSeeing", false)
cfxArtilleryUI.allRanging = cfxZones.getBoolFromZoneProperty(theZone, "allRanging", false)
cfxArtilleryUI.allTiming = cfxZones.getBoolFromZoneProperty(theZone, "allTiming", false)
end
--
-- Start
--
function cfxArtilleryUI.start()
if not dcsCommon.libCheck("cfx Artillery UI",
cfxArtilleryUI.requiredLibs) then
return false
end
-- read config
cfxArtilleryUI.readConfigZone()
-- iterate existing groups so we have a start situation
-- now iterate through all player groups and install the Assault Troop Menu
local allPlayerGroups = cfxPlayerGroups -- cfxPlayerGroups is a global, don't fuck with it!
-- contains per group player record. Does not resolve on unit level!
for gname, pgroup in pairs(allPlayerGroups) do
local theUnit = pgroup.primeUnit -- get any unit of that group
cfxArtilleryUI.setCommsMenuForUnit(theUnit) -- set up
end
-- now install the new group notifier to install Assault Troops menu
cfxPlayer.addMonitor(cfxArtilleryUI.playerChangeEvent)
-- run an update loop for the target menu
cfxArtilleryUI.update()
trigger.action.outText("cf/x cfxArtilleryUI v" .. cfxArtilleryUI.version .. " started", 30)
return true
end
--
-- GO GO GO
--
if not cfxArtilleryUI.start() then
trigger.action.outText("Loading cf/x Artillery UI aborted.", 30)
cfxArtilleryUI = nil
end
--[[--
TODO: transition times based on distance - requires real bound arty first
DONE: ui for smoking target zone: list ten closest zones, and provide menu to smoke zone
--]]--

View File

@ -0,0 +1,448 @@
cfxArtilleryZones = {}
cfxArtilleryZones.version = "2.0.1"
cfxArtilleryZones.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
}
cfxArtilleryZones.verbose = false
--[[--
Version History
1.0.0 - initial version
1.0.1 - simSmokeZone
2.0.0 - zone attributes for shellNum, shellVariance,
cooldown, addMark, transitionTime
- doFireAt method
- simFireAt now calls doFireAt
- added all params to crteateArtilleryTarget
- createArtillerTarget replaced createArtilleryZone
- addMark now used so arty zones can be hidden on map
- added triggerFlag attribute
- update now fires every time when flag changes
2.0.1 - added verbose setting
- base accuracy now derived from radius
- added coalition check for ZonesInRange
- att transition time to zone info mark
- made compatible with linked zones
- added silent attribute
- added transition time to arty command chatter
Artillery Target Zones *** EXTENDS ZONES ***
Target Zones for artillery. Can determine which zones are in range and visible and then handle artillery barrage to this zone
Copyright (c) 2021, 2022 by Christian Franz and cf/x AG
USAGE
Via ME: Add the relevant attributes to the zone
Via Script: Use createArtilleryTarget()
Callbacks
when fire at target is invoked, a callback can be
invoked so your code knows that fire control has been
given a command or that projectiles are impacting.
Signature
callback(rason, zone, data), with
reason: 'firing' - fire command given for zone
'impact' a projectile has hit
zone: artilleryZone
data: empty on 'fire'
.point where impact point
.strength power of explosion
--]]--
cfxArtilleryZones.artilleryZones = {}
cfxArtilleryZones.updateDelay = 1 -- every second
--
-- C A L L B A C K S
--
cfxArtilleryZones.callbacks = {}
function cfxArtilleryZones.addCallback(theCallback)
table.insert(cfxArtilleryZones.callbacks, theCallback)
end
function cfxArtilleryZones.invokeCallbacksFor(reason, zone, data)
for idx, theCB in pairs (cfxArtilleryZones.callbacks) do
theCB(reason, zone, data)
end
end
function cfxArtilleryZones.demoCallback(reason, zone, data)
-- reason: 'fire' or 'impact'
-- fire has no data, impact has data.point and data.strength
end
function cfxArtilleryZones.createArtilleryTarget(name, point, coalition, spotRange, transitionTime, baseAccuracy, shellNum, shellStrength, shellVariance, triggerFlag, addMark, cooldown, silent, autoAdd) -- was: createArtilleryZone, changed params list
if not point then return end
if not autoAdd then autoAdd = false end
if not coalition then coalition = 0 end
if not spotRange then spotRange = 3000 end
if not shellStrength then shellStrength = 500 end
if not transitionTime then transitionTime = 20 end
if not shellNum then shellNum = 17 end
if not addMark then addMark = false end
if not name then name = "dftZName" end
if not shellVariance then shellVariance = 0.2 end
if not cooldown then cooldown = 120 end
if not baseAccuracy then baseAccuracy = 100 end
if not silent then silent = false end
name = cfxZones.createUniqueZoneName(name)
local newZone = cfxZones.createSimpleZone(name,
point,
100,
autoAdd)
newZone.spotRange = spotRange
newZone.coalition = coalition
newZone.landHeight = land.getHeight({x = newZone.point.x, y= newZone.point.z})
newZone.transitionTime = transitionTime
newZone.shellNum = shellNum
newZone.shellStrength = shellStrength
newZone.triggerFlag = triggerFlag -- can be nil
if triggerFlag then
newZone.lastTriggerValue = trigger.misc.getUserFlag(triggerFlag) -- save last value
end
newZone.addMark = addMark
if autoAdd then cfxArtilleryZones.addArtilleryZone(newZone) end
newZone.shellVariance = shellVariance
newZone.cooldown = cooldown
newZone.silent = silent
end
function cfxArtilleryZones.processArtilleryZone(aZone)
aZone.artilleryTarget = cfxZones.getStringFromZoneProperty(aZone, "artilleryTarget", aZone.name)
aZone.coalition = cfxZones.getCoalitionFromZoneProperty(aZone, "coalition", 0) -- side that marks it on map, and who fires arty
aZone.spotRange = cfxZones.getNumberFromZoneProperty(aZone, "spotRange", 3000) -- FO max range to direct fire
aZone.shellStrength = cfxZones.getNumberFromZoneProperty(aZone, "shellStrength", 500) -- power of shells (strength)
aZone.shellNum = cfxZones.getNumberFromZoneProperty(aZone, "shellNum", 17) -- number of shells in bombardment
aZone.transitionTime = cfxZones.getNumberFromZoneProperty(aZone, "transitionTime", 20) -- average time of travel for projectiles
aZone.addMark = cfxZones.getBoolFromZoneProperty(aZone, "addMark", true) -- note: defaults to true
aZone.shellVariance = cfxZones.getNumberFromZoneProperty(aZone, "shellVariance", 0.2) -- strength of explosion can vary by +/- this amount
if cfxZones.hasProperty(aZone, "f?") then
aZone.triggerFlag = cfxZones.getStringFromZoneProperty(aZone, "f?", "none")
end
if cfxZones.hasProperty(aZone, "triggerFlag") then
aZone.triggerFlag = cfxZones.getStringFromZoneProperty(aZone, "triggerFlag", "none")
end
if aZone.triggerFlag then
aZone.lastTriggerValue = trigger.misc.getUserFlag(aZone.triggerFlag) -- save last value
end
aZone.cooldown =cfxZones.getNumberFromZoneProperty(aZone, "cooldown", 120) -- seconds
aZone.baseAccuracy = cfxZones.getNumberFromZoneProperty(aZone, "baseAccuracy", aZone.radius) -- meters from center radius shell impact
-- use zone radius as mase accuracy for simple placement
aZone.silent = cfxZones.getBoolFromZoneProperty(aZone, "silent", false)
end
function cfxArtilleryZones.addArtilleryZone(aZone)
-- add landHeight to this zone
aZone.landHeight = land.getHeight({x = aZone.point.x, y= aZone.point.z})
-- mark it on the map
aZone.artyCooldownTimer = -1000
cfxArtilleryZones.placeMarkForSide(aZone.point, aZone.coalition, aZone.name .. ", FO=" .. aZone.spotRange .. "m" .. ", tt=" .. aZone.transitionTime)
table.insert(cfxArtilleryZones.artilleryZones, aZone)
end
function cfxArtilleryZones.findArtilleryZoneNamed(aName)
aZone = cfxZones.getZoneByName(aName)
if not aZone then return nil end
-- check if it is an arty zone
if not aZone.artilleryTarget then return nil end
-- all is well
return aZone
end
function cfxArtilleryZones.removeArtilleryZone(aZone)
if type(aZone) == "string" then
aZone = cfxArtilleryZones.findArtilleryZoneNamed(aZone)
end
if not aZone then return end
-- now create new table
local filtered = {}
for idx, theZone in pairs(cfxArtilleryZones.artilleryZones) do
if theZone ~= aZone then
table.insert(filtered, theZone)
end
end
cfxArtilleryZones.artilleryZones = filtered
end
function cfxArtilleryZones.artilleryZonesInRangeOfUnit(theUnit)
if not theUnit then return {} end
if not theUnit:isExist() then return {} end
local myCoalition = theUnit:getCoalition()
local zonesInRange = {}
local p = theUnit:getPoint()
for idx, aZone in pairs(cfxArtilleryZones.artilleryZones) do
-- is it one of mine?
if aZone.coalition == myCoalition then
-- is it close enough?
local zP = cfxZones.getPoint(aZone)
aZone.landHeight = land.getHeight({x = zP.x, y= zP.z})
local zonePoint = {x = zP.x, y = aZone.landHeight, z = zP.z}
local d = dcsCommon.dist(p,zonePoint)
if d < aZone.spotRange then
-- LOS check
if land.isVisible(p, zonePoint) then
-- yeah, add to list
table.insert(zonesInRange, aZone)
end
end
end
end
return zonesInRange
end
--
-- MARK ON MAP
--
cfxArtilleryZones.uuidCount = 0
function cfxArtilleryZones.uuid()
cfxArtilleryZones.uuidCount = cfxArtilleryZones.uuidCount + 1
return cfxArtilleryZones.uuidCount
end
function cfxArtilleryZones.placeMarkForSide(location, theSide, theDesc)
local theID = cfxArtilleryZones.uuid()
local theDesc = "ARTY: ".. theDesc
trigger.action.markToCoalition(
theID,
theDesc,
location,
theSide,
false,
nil)
return theID
end
function cfxArtilleryZones.removeMarkForArgs(args)
local theID = args[1]
trigger.action.removeMark(theID)
end
--
-- FIRE AT A ZONE
--
--
-- BOOM command
--
function cfxArtilleryZones.doBoom(args)
trigger.action.explosion(args.point, args.strength)
data = {}
data.point = args.point
data.strength = args.strength
cfxArtilleryZones.invokeCallbacksFor('impact', args.zone, data)
end
function cfxArtilleryZones.doFireAt(aZone, maxDistFromCenter)
if type(aZone) == "string" then
local mZone = cfxArtilleryZones.findArtilleryZoneNamed(aZone)
aZone = mZone
end
if not aZone then return end
if not maxDistFromCenter then maxDistFromCenter = aZone.baseAccuracy end
local accuracy = maxDistFromCenter
local zP = cfxZones.getPoint(aZone)
aZone.landHeight = land.getHeight({x = zP.x, y= zP.z})
local center = {x=zP.x, y=aZone.landHeight, z=zP.z} -- center of where shells hit
local shellNum = aZone.shellNum
local shellBaseStrength = aZone.shellStrength
local shellVariance = aZone.shellVariance
local transitionTime = aZone.transitionTime
for i=1, shellNum do
local thePoint = dcsCommon.randomPointInCircle(accuracy, 0, center.x, center.z)
thePoint.y = land.getHeight({x=thePoint.x, y=thePoint.z})
local boomArgs = {}
local strVar = shellBaseStrength * shellVariance
strVar = strVar * (2 * dcsCommon.randomPercent() - 1.0) -- go from -1 to 1
boomArgs.strength = shellBaseStrength + strVar
thePoint.y = land.getHeight({x = thePoint.x, y = thePoint.z}) + 1 -- elevate to ground height + 1
boomArgs.point = thePoint
boomArgs.zone = aZone
local timeVar = 5 * (2 * dcsCommon.randomPercent() - 1.0) -- +/- 1.5 seconds
if timeVar < 0 then timeVar = -timeVar end
-- if transitionTime + timeVar < 0 then timeVar = -timeVar end
--boomArgs.tDelta = timeVar
timer.scheduleFunction(cfxArtilleryZones.doBoom, boomArgs, timer.getTime() + transitionTime + timeVar)
end
-- invoke callbacks
cfxArtilleryZones.invokeCallbacksFor('fire', aZone, {})
end
function cfxArtilleryZones.simFireAtZone(aZone, aGroup, dist)
-- all very simple. We simulate shellNum
-- projectiles impacting in aZone
-- before calling doFire, we calculate accuracy
-- for given dist
if not dist then dist = aZone.spotRange end
--local transitionTime = 20 -- seconds until shells hit
--transitionTime = aZone.transitionTime
--local shellNum = 17
--if aZone.shellNum then shellNum = aZone.shellNum end
--local shellBaseStrength = 500
--if aZone.shellStrength then
local shellBaseStrength = aZone.shellStrength
--local shellVariance = 0.2 -- +/-10%
--shellVariance = aZone.shellVariance
--local center = {x=aZone.point.x, y=aZone.landHeight, z=aZone.point.z} -- center of where shells hit
local maxAccuracy = 100 -- m radius when close
local minAccuracy = 500 -- m radius whan at max sport dist
local currAccuracy = minAccuracy
if dist <= 1000 then
currAccuracy = maxAccuracy
else
local percent = (dist-1000) / (aZone.spotRange-1000)
currAccuracy = dcsCommon.lerp(maxAccuracy, minAccuracy, percent)
end
currAccuracy = math.floor(currAccuracy)
cfxArtilleryZones.doFireAt(aZone, currAccuracy)
--[[-- Old code follows
for i=1, shellNum do
local thePoint = dcsCommon.randomPointInCircle(currAccuracy, 0, center.x, center.z)
thePoint.y = land.getHeight({x=thePoint.x, y=thePoint.z})
local boomArgs = {}
local strVar = shellBaseStrength * shellVariance
strVar = strVar * (2 * dcsCommon.randomPercent() - 1.0) -- go from -1 to 1
boomArgs.strength = shellBaseStrength + strVar
thePoint.y = land.getHeight({x = thePoint.x, y = thePoint.z}) + 1 -- elevate to ground height + 1
boomArgs.point = thePoint
local timeVar = 5 * (2 * dcsCommon.randomPercent() - 1.0) -- +/- 1.5 seconds
boomArgs.tDelta = timeVar
timer.scheduleFunction(cfxArtilleryZones.doBoom, boomArgs, timer.getTime() + transitionTime + timeVar)
end
--]]--
aZone.artyCooldownTimer = timer.getTime() + aZone.cooldown -- 120 -- 2 minutes reload
if not aZone.silent then
local addInfo = " with d=" .. dist .. ", var = " .. currAccuracy .. " pB=" .. shellBaseStrength .. " tt=" .. aZone.transitionTime
trigger.action.outTextForCoalition(aGroup:getCoalition(), "Artillery firing on ".. aZone.name .. addInfo, 30)
end
--trigger.action.smoke(center, 2) -- mark location visually
end
function cfxArtilleryZones.simSmokeZone(aZone, aGroup, aColor)
-- this is simsmoke: transition time is fixed, and we do not
-- use arty units. all very simple. we merely place smoke on
-- ground
if not aColor then aColor = "red" end
if type(aColor) == "string" then
aColor = dcsCommon.smokeColor2Num(aColor)
end
local zP = cfxZones.getPoint(aZone)
aZone.landHeight = land.getHeight({x = zP.x, y= zP.z})
local transitionTime = aZone.transitionTime --17 -- seconds until phosphor lands
local center = {x = zP.x,
y =aZone.landHeight + 3,
z = zP.z
} -- center of where shells hit
-- we now can 'dirty' the position by something. not yet
local currAccuracy = 200
local thePoint = dcsCommon.randomPointInCircle(currAccuracy, 50, center.x, center.z)
-- thePoint.y = land.getHeight({ x = thePoint.x, y = thePoint.z})
timer.scheduleFunction(cfxArtilleryZones.doSmoke, {thePoint, aColor}, timer.getTime() + transitionTime)
if not aGroup then return end
if aZone.silent then return end
trigger.action.outTextForCoalition(aGroup:getCoalition(), "Artillery firing single phosphor round at ".. aZone.name, 30)
end
function cfxArtilleryZones.doSmoke(args)
local thePoint = args[1]
local aColor = args[2]
dcsCommon.markPointWithSmoke(thePoint, aColor)
end
--
-- UPDATE
--
function cfxArtilleryZones.update()
-- call me in a couple of minutes to 'rekindle'
timer.scheduleFunction(cfxArtilleryZones.update, {}, timer.getTime() + cfxArtilleryZones.updateDelay)
-- iterate all zones to see if a trigger has changed
for idx, aZone in pairs(cfxArtilleryZones.artilleryZones) do
if aZone.triggerFlag then
local currTriggerVal = trigger.misc.getUserFlag(aZone.triggerFlag)
if currTriggerVal ~= aZone.lastTriggerValue
then
-- a triggered release!
cfxArtilleryZones.doFireAt(aZone) -- all from zone vars!
if cfxArtilleryZones.verbose then
local addInfo = " with var = " .. aZone.baseAccuracy .. " pB=" .. aZone.shellStrength
trigger.action.outText("Artillery T-Firing on ".. aZone.name .. addInfo, 30)
end
aZone.lastTriggerValue = currTriggerVal
end
end
end
end
--
-- START
--
function cfxArtilleryZones.start()
if not dcsCommon.libCheck("cfx Artillery Zones",
cfxArtilleryZones.requiredLibs) then
return false
end
-- collect all spawn zones
local attrZones = cfxZones.getZonesWithAttributeNamed("artilleryTarget")
-- now create a spawner for all, add them to the spawner updater, and spawn for all zones that are not
-- paused
for k, aZone in pairs(attrZones) do
cfxArtilleryZones.processArtilleryZone(aZone) -- process attribute and add to zone
cfxArtilleryZones.addArtilleryZone(aZone) -- remember it so we can smoke it
end
-- start update loop
cfxArtilleryZones.update()
-- say hi
trigger.action.outText("cfx Artillery Zones v" .. cfxArtilleryZones.version .. " started.", 30)
return true
end
-- let's go
if not cfxArtilleryZones.start() then
trigger.action.outText("cf/x Artillery Zones aborted: missing libraries", 30)
cfxArtilleryZones = nil
end
--[[--
TODO: link artillery units that are starting fire, stop when all inside is destroyed
TODO: "Free" link: look for closest artillery zone that is not cooling down or engaged with diofferent targets and is in range
DONE: smoke target zon
DONE: add smoke to UI menu
DONE: move doBoom from demon to this module
DONE: trigger on flag
DONE: inflight time for arty projectiles
DONE: invoke callback for firing
TODO: duration param for bombardment
TODO: silent attribute
--]]--

171
modules/cfxCargoManager.lua Normal file
View File

@ -0,0 +1,171 @@
cfxCargoManager = {}
cfxCargoManager.version = "1.0.2"
cfxCargoManager.ups = 1 -- updates per second
--[[--
Version History
- 1.0.0 - initial version
- 1.0.1 - isexist check on remove cargo
- 1.0.2 - ability to access a cargo status
Cargo Manager is a module that watches cargo that is handed for
management, and initiates callbacks when a cargo event happens
Cargo events are (string)
- lifted (cargo is lifted from ground: ground-->air transition)
- grounded (cargo was put on the ground: air-->ground transition)
- disappeared (cargo was deleted): isExits() failed
- dead (cargo was destroyed) life < 1
- new (cargo was added to manager)
- remove (cargo was removed from manager)
callback signature
theCB(event, theCargoObject, cargoName)
--]]--
cfxCargoManager.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
}
cfxCargoManager.callbacks = {}
cfxCargoManager.allCargo = {}
cfxCargoManager.cargoStatus = {}
cfxCargoManager.cargoPosition = {}
-- callback management
cfxCargoManager.monitor = false
function cfxCargoManager.addCallback(cb)
table.insert(cfxCargoManager.callbacks, cb)
end
function cfxCargoManager.invokeCallback(event, obj, name)
for idx, cb in pairs(cfxCargoManager.callbacks) do
cb(event, obj, name)
end
end
function cfxCargoManager.standardCallback(event, object, name)
trigger.action.outText("Cargo event <" .. event .. "> for " .. name, 30)
end
-- get cargo status
function cfxCargoManager.getCargoStatusFor(theCargoObject)
if not theCargoObject then return nil end
local cargoName = ""
if type(theCargoObject) == "string" then
cargoName = theCargoObject
else
cargoName = theCargoObject:getName()
end
if not cargoName then return nil end
return cargoStatus[cargoName]
end
-- add / remove cargo
function cfxCargoManager.addCargo(theCargoObject)
if not theCargoObject then return end
if not theCargoObject:isExist() then return end
local cargoName = theCargoObject:getName()
cfxCargoManager.allCargo[cargoName] = theCargoObject
cfxCargoManager.cargoStatus[cargoName] = "new"
-- cfxCargoManager.cargoStatus[cargoName] = nil
cfxCargoManager.cargoPosition[cargoName] = theCargoObject:getPoint()
cfxCargoManager.invokeCallback("new", theCargoObject, cargoName)
end
function cfxCargoManager.removeCargoByName(cargoName)
if not cargoName then return end
local theCargoObject = cfxCargoManager.allCargo[cargoName]
cfxCargoManager.invokeCallback("remove", theCargoObject, cargoName)
cfxCargoManager.allCargo[cargoName] = nil
cfxCargoManager.cargoStatus[cargoName] = nil
cfxCargoManager.cargoPosition[cargoName] = nil
end
function cfxCargoManager.removeCargo(theCargoObject)
if not theCargoObject then return end
if not theCargoObject:isExist() then return end
local cargoName = theCargoObject:getName()
cfxCargoManager.removeCargoByName(cargoName)
end
-- get all cargo gets all cargos (default) or all cargos
-- that have a certain state, e.g. lifted
function cfxCargoManager.getAllCargo(filterForState)
local theCargo = {}
for name, cargo in pairs(cfxCargoManager.allCargo) do
if (filterForState == nil) or
(filterForState == cfxCargoManager.cargoStatus[name])
then
table.insert(theCargo, cargo)
end
end
return theCargo
end
-- update loop
function cfxCargoManager.determineCargoStatus(cargo)
if not cargo then return "disappeared" end
if not cargo:isExist() then return "disappeared" end
-- note that inAir() currently always returns false
local name = cargo:getName()
local oldPos = cfxCargoManager.cargoPosition[name]
local newPos = cargo:getPoint()
cfxCargoManager.cargoPosition[name] = newPos -- update
local delta = dcsCommon.dist(oldPos, newPos)
-- if cargo:inAir() then return "lifted" end -- currentl doesn't work
if delta > 1 then return "lifted" end -- moving
if cargo:getLife() < 1 then return "dead" end
local agl = dcsCommon.getUnitAGL(cargo)
if agl > 5 then return "lifted" end -- not moving but still above ground. good hover!
-- if velocity > 1 m/s this thing is moving
-- if dcsCommon.vMag(cargo:getVelocity()) > 1 then return "lifted" end -- currently doesn't work
-- this thing simply sits on the ground
return "grounded"
end
function cfxCargoManager.update()
-- re-schedule in ups
timer.scheduleFunction(cfxCargoManager.update, {}, timer.getTime() + 1/cfxCargoManager.ups)
-- iterate all cargos
local newCargoManifest = {}
for name, cargo in pairs(cfxCargoManager.allCargo) do
local newStatus = cfxCargoManager.determineCargoStatus(cargo)
local oldStatus = cfxCargoManager.cargoStatus[name]
if newStatus ~= oldStatus then
cfxCargoManager.invokeCallback(newStatus, cargo, name)
cfxCargoManager.cargoStatus[name] = newStatus
end
if newStatus == "dead" or newStatus == "disappeared" then
cfxCargoManager.removeCargoByName(name) -- we are changing what we iterate?
end
end
end
-- start up
function cfxCargoManager.start()
if not dcsCommon.libCheck("cfx Cargo Manager",
cfxCargoManager.requiredLibs) then
return false
end
-- start update loop
cfxCargoManager.update()
-- say hi
trigger.action.outText("cfx Cargo Manager v" .. cfxCargoManager.version .. " started.", 30)
return true
end
-- let's go
if not cfxCargoManager.start() then
trigger.action.outText("cf/x Cargo Manager aborted: missing libraries", 30)
cfxCargoManager = nil
elseif cfxCargoManager.monitor then
cfxCargoManager.addCallback(cfxCargoManager.standardCallback)
end

View File

@ -0,0 +1,256 @@
cfxCargoReceiver = {}
cfxCargoReceiver.version = "1.1.0"
cfxCargoReceiver.ups = 1 -- once a second
cfxCargoReceiver.maxDirectionRange = 500 -- in m. distance when cargo manager starts talking to pilots who are carrying that cargo
cfxCargoReceiver.requiredLibs = {
"dcsCommon", -- always
"cfxPlayer", -- for directions
"cfxZones", -- Zones, of course
"cfxCargoManager", -- will notify me on a cargo event
}
--[[--
Version history
- 1.0.0 initial vbersion
- 1.1.0 added flag manipulation options
no negative agl on announcement
silent attribute
CargoReceiver is a zone enhancement you use to be automatically
notified if a cargo was delivered inside the zone.
It also provides BRA when in range to a cargo receiver
*** EXTENDS ZONES
Callback signature:
cb(event, obj, name, zone) with
- event being string, currently defined: 'deliver'
- obj being the cargo object
- name being cargo object name
- zone in which cargo was dropped (if dropped)
--]]--
cfxCargoReceiver.receiverZones = {}
function cfxCargoReceiver.processReceiverZone(aZone) -- process attribute and add to zone
-- since the attribute is there, simply set the zones
-- isCargoReceiver flag and we are good
aZone.isCargoReceiver = true
-- we can add additional processing here
aZone.autoRemove = cfxZones.getBoolFromZoneProperty(aZone, "autoRemove", false) -- maybe add a removedelay
aZone.silent = cfxZones.getBoolFromZoneProperty(aZone, "silent", false)
--trigger.action.outText("+++rcv: recognized receiver zone: " .. aZone.name , 30)
-- same integration as object destruct detector for flags
if cfxZones.hasProperty(aZone, "setFlag") then
aZone.setFlag = cfxZones.getStringFromZoneProperty(aZone, "setFlag", "999")
end
if cfxZones.hasProperty(aZone, "f=1") then
aZone.setFlag = cfxZones.getStringFromZoneProperty(aZone, "f=1", "999")
end
if cfxZones.hasProperty(aZone, "clearFlag") then
aZone.clearFlag = cfxZones.getStringFromZoneProperty(aZone, "clearFlag", "999")
end
if cfxZones.hasProperty(aZone, "f=0") then
aZone.clearFlag = cfxZones.getStringFromZoneProperty(aZone, "f=0", "999")
end
if cfxZones.hasProperty(aZone, "increaseFlag") then
aZone.increaseFlag = cfxZones.getStringFromZoneProperty(aZone, "increaseFlag", "999")
end
if cfxZones.hasProperty(aZone, "f+1") then
aZone.increaseFlag = cfxZones.getStringFromZoneProperty(aZone, "f+1", "999")
end
if cfxZones.hasProperty(aZone, "decreaseFlag") then
aZone.decreaseFlag = cfxZones.getStringFromZoneProperty(aZone, "decreaseFlag", "999")
end
if cfxZones.hasProperty(aZone, "f-1") then
aZone.decreaseFlag = cfxZones.getStringFromZoneProperty(aZone, "f-1", "999")
end
end
function cfxCargoReceiver.addReceiverZone(aZone)
if not aZone then return end
cfxCargoReceiver.receiverZones[aZone.name] = aZone
end
-- callback handling
cfxCargoReceiver.callbacks = {}
function cfxCargoReceiver.addCallback(cb)
table.insert(cfxCargoReceiver.callbacks, cb)
end
function cfxCargoReceiver.invokeCallback(event, obj, name, zone)
for idx, cb in pairs(cfxCargoReceiver.callbacks) do
cb(event, obj, name, zone)
end
end
function cfxCargoReceiver.standardCallback(event, object, name, zone)
trigger.action.outText("Cargo received event <" .. event .. "> for " .. name .. " in " .. zone.name , 30)
end
--
-- cargo event happened. Called by Cargo Manager
--
function cfxCargoReceiver.cargoEvent(event, object, name)
--trigger.action.outText("Cargo Receiver: event <" .. event .. "> for " .. name, 30)
if not event then return end
if event == "grounded" then
--trigger.action.outText("+++rcv: grounded for " .. name, 30)
-- this is actually the only one that interests us
if not object then
--trigger.action.outText("+++rcv: " .. name .. " has null object", 30)
return
end
if not object:isExist() then
--trigger.action.outText("+++rcv: " .. name .. " no longer exists", 30)
return
end
loc = object:getPoint()
-- now invoke callbacks for all zones
-- this is in
for name, aZone in pairs(cfxCargoReceiver.receiverZones) do
if cfxZones.pointInZone(loc, aZone) then
cfxCargoReceiver.invokeCallback("deliver", object, name, aZone)
-- set flags as indicated
if aZone.setFlag then
trigger.action.setUserFlag(aZone.setFlag, 1)
end
if aZone.clearFlag then
trigger.action.setUserFlag(aZone.clearFlag, 0)
end
if aZone.increaseFlag then
local val = trigger.misc.getUserFlag(aZone.increaseFlag) + 1
trigger.action.setUserFlag(aZone.increaseFlag, val)
end
if aZone.decreaseFlag then
local val = trigger.misc.getUserFlag(aZone.decreaseFlag) - 1
trigger.action.setUserFlag(aZone.decreaseFlag, val)
end
--trigger.action.outText("+++rcv: " .. name .. " delivered in zone " .. aZone.name, 30)
--trigger.action.outSound("Quest Snare 3.wav")
if aZone.autoRemove then
-- maybe schedule this in a few seconds?
object:destroy()
end
end
end
end
end
-- update loop
function cfxCargoReceiver.update()
-- schedule me in 1/ups
timer.scheduleFunction(cfxCargoReceiver.update, {}, timer.getTime() + 1/cfxCargoReceiver.ups)
-- we now get all cargos that are in the air
local liftedCargos = cfxCargoManager.getAllCargo("lifted")
-- new we see if any of these are close to a delivery zone
for idx, aCargo in pairs(liftedCargos) do
local thePoint = aCargo:getPoint()
local receiver, delta = cfxZones.getClosestZone(
thePoint,
cfxCargoReceiver.receiverZones -- must be indexed by name
)
-- we now check if we are in 'speaking range' and receiver can talk
if (receiver.silent == false) and
(delta < cfxCargoReceiver.maxDirectionRange) then
-- this cargo can be talked down.
-- find the player unit that is closest to in in hopes
-- that that is the one carrying it
local allPlayers = cfxPlayer.getAllPlayers() -- idx by name
for pname, info in pairs(allPlayers) do
-- iterate all player units
local closestUnit = nil
local minDelta = math.huge
local theUnit = info.unit
if theUnit:isExist() then
local uPoint = theUnit:getPoint()
local currDelta = dcsCommon.dist(thePoint, uPoint)
if currDelta < minDelta then
minDelta = currDelta
closestUnit = theUnit
end
end
-- see if we got a player unit close enough
if closestUnit ~= nil and minDelta < 100 then
-- get group and communicate the relevant info
local theGroup = closestUnit:getGroup()
local insideZone = cfxZones.pointInZone(thePoint, receiver)
local message = aCargo:getName()
if insideZone then
message = message .. " is inside delivery zone " .. receiver.name
else
-- get bra to center
local ownHeading = dcsCommon.getUnitHeadingDegrees(closestUnit)
local oclock = dcsCommon.clockPositionOfARelativeToB(
receiver.point,
thePoint,
ownHeading) .. " o'clock"
message = receiver.name .. " is " .. math.floor(delta) .. "m at your " .. oclock
end
-- add agl
local agl = dcsCommon.getUnitAGL(aCargo)
if agl < 0 then agl = 0 end
message = message .. ". Cargo is " .. math.floor(agl) .. "m AGL."
-- now say so. 5 second staying power, one second override
-- full erase screen
trigger.action.outTextForGroup(theGroup:getID(),
message, 5, true)
else
-- cargo in range, no player
end
end
end
end
end
--
-- GO!
--
function cfxCargoReceiver.start()
if not dcsCommon.libCheck("cfx Cargo Receiver",
cfxCargoReceiver.requiredLibs) then
return false
end
-- scan all zones for cargoReceiver flag
local attrZones = cfxZones.getZonesWithAttributeNamed("cargoReceiver")
-- now create a spawner for all, add them to the spawner updater, and spawn for all zones that are not
-- paused
for k, aZone in pairs(attrZones) do
cfxCargoReceiver.processReceiverZone(aZone) -- process attribute and add to zone
cfxCargoReceiver.addReceiverZone(aZone) -- remember it so we can smoke it
end
-- tell cargoManager that I want to be involved
cfxCargoManager.addCallback(cfxCargoReceiver.cargoEvent)
-- start update loop
cfxCargoReceiver.update()
-- say hi
trigger.action.outText("cfx Cargo Receiver v" .. cfxCargoReceiver.version .. " started.", 30)
return true
end
-- let's go
if not cfxCargoReceiver.start() then
trigger.action.outText("cf/x Cargo Receiver aborted: missing libraries", 30)
cfxCargoReceiver = nil
end
-- TODO: config zone for talking down pilots
-- TODO: f+/f-/f=1/f=0

515
modules/cfxCommander.lua Normal file
View File

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

1138
modules/cfxGroudTroops.lua Normal file

File diff suppressed because it is too large Load Diff

158
modules/cfxGroups.lua Normal file
View File

@ -0,0 +1,158 @@
cfxGroups = {}
cfxGroups.version = "1.1.0"
--[[--
Module to read Unit data from DCS and make it available to scripts
DOES NOT KEEP TRACK OF MISSION-CREATED GROUPS!!!!
Main use is to access player groups for slot blocking etc since these
groups can't be allocated dynamically
Version history
1.0.0 - initial version
1.1.0 - for each player unit, store point(x, 0, y), and action for first WP, as well as name
--]]--
cfxGroups.groups = {} -- all groups, indexed by name
--[[-- group objects are
{
name= "",
coalition = "" (red, blue, neutral),
coanum = # (0, 1, 2 for neutral, red, blue)
category = "" (helicopter, ship, plane, vehicle, static),
hasPlayer = true/false,
playerUnits = {} (for each player unit in group: name, point, action)
}
--]]--
function cfxGroups.fetchAllGroupsFromDCS()
-- a mission is a lua table that is loaded by executing the miz. it builds
-- the environment mission table, accessible as env.mission
-- iterate the "coalition" table of the mission (note: NOT coalitionS)
-- inspired by mist, GIANT tip o'the hat to Grimes!
for coa_name_miz, coa_data in pairs(env.mission.coalition) do -- iterate all coalitions
local coa_name = coa_name_miz
if string.lower(coa_name_miz) == 'neutrals' then -- convert "neutrals" to "neutral", singular
coa_name = 'neutral'
end
-- directly convert coalition into number for easier access later
local coaNum = 0
if coa_name == "red" then coaNum = 1 end
if coa_name == "blue" then coaNum = 2 end
if type(coa_data) == 'table' then
if coa_data.country then -- make sure there a country table for this coalition
for cntry_id, cntry_data in pairs(coa_data.country) do -- iterate all countries for this
local countryName = string.lower(cntry_data.name)
if type(cntry_data) == 'table' then --just making sure
for obj_type_name, obj_type_data in pairs(cntry_data) do
if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check
local category = obj_type_name
if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group!
for group_num, group_data in pairs(obj_type_data.group) do
if group_data and group_data.units and type(group_data.units) == 'table' then --making sure again- this is a valid group
local groupName = group_data.name
if env.mission.version > 7 then -- translate raw to actual
groupName = env.getValueDictByKey(groupName)
end
local hasPlayer = false
local playerUnits = {}
for unit_num, unit_data in pairs(group_data.units) do -- iterate units
-- see if there is at least one player in group
if unit_data.skill then
if unit_data.skill == "Client" or unit_data.skill == "Player" then
-- this is player unit. save it, remember
hasPlayer = true
local playerData = {}
playerData.name = unit_data.name
playerData.point = {}
playerData.point.x = unit_data.x
playerData.point.y = 0
playerData.point.z = unit_data.y
playerData.action = "none" -- default
-- access initial waypoint data by 'reaching up'
-- into group data and extract route.points[1]
if group_data.route and group_data.route.points and (#group_data.route.points > 0) then
playerData.action = group_data.route.points[1].action
end
table.insert(playerUnits, playerData)
end
end
end --for all units in group
local entry = {}
entry.name = groupName
entry.coalition = coa_name
entry.coaNum = coaNum
entry.category = category
entry.hasPlayer = hasPlayer
entry.playerUnits = playerUnits
-- add to db
cfxGroups.groups[groupName] = entry
end --if has group_data and group_data.units then
end --for all groups in category
end --if has category data
end --if plane, helo etc... category
end --for all objects in country
end --if has country data
end --for all countries in coalition
end --if coalition has country table
end -- if there is coalition data
end --for all coalitions in mission
end
-- simply dump all groups to the screen
function cfxGroups.showAllGroups()
for gName, gData in pairs (cfxGroups.groups) do
local isP = "(NPC)"
if gData.hasPlayer then isP = "*PLAYER GROUP (".. #gData.playerUnits ..")*" end
trigger.action.outText(gData.name.. ": " .. isP .. " - " .. gData.category .. ", F:" .. gData.coalition
.. " (" .. gData.coaNum .. ")", 30)
end
end
-- return all cfxGroups that can have players in them
-- includes groups that currently are not or not anymore alive
function cfxGroups.getPlayerGroup()
local playerGroups = {}
for gName, gData in pairs (cfxGroups.groups) do
if gData.hasPlayer then
table.insert(playerGroups, gData)
end
end
return playerGroups
end
-- return all group names that can have players in them
-- includes groups that currently are not or not anymore alive
function cfxGroups.getPlayerGroupNames()
local playerGroups = {}
for gName, gData in pairs (cfxGroups.groups) do
if gData.hasPlayer then
table.insert(playerGroups, gName)
end
end
return playerGroups
end
function cfxGroups.start()
cfxGroups.fetchAllGroupsFromDCS() -- read all groups from mission.
-- cfxGroups.showAllGroups()
trigger.action.outText("cfxGroups version " .. cfxGroups.version .. " started", 30)
return true
end
cfxGroups.start()

826
modules/cfxHeloTroops.lua Normal file
View File

@ -0,0 +1,826 @@
cfxHeloTroops = {}
cfxHeloTroops.version = "2.1.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
--
-- 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.requiredLibs = {
"dcsCommon", -- common is of course needed for everything
-- pretty stupid to check for this since we
-- need common to invoke the check, but anyway
"cfxZones", -- Zones, of course
"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
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
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
conf.troopsOnBoard = {} -- table with the following
conf.troopsOnBoard.name = "***reset***"
conf.dropFormation = "circle_out" -- may be chosen later?
end
function cfxHeloTroops.createDefaultConfig(theUnit)
local conf = {}
cfxHeloTroops.resetConfig(conf)
conf.myMainMenu = nil -- this is where the main menu for group will be stored
conf.myCommands = nil -- this is where we put all teh commands in
return conf
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)
return nil
end
local c = cfxHeloTroops.unitConfigs[theUnit:getName()]
if not c then
c = cfxHeloTroops.createDefaultConfig(theUnit)
cfxHeloTroops.unitConfigs[theUnit:getName()] = c
end
return c
end
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 cfxPlayer.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
--
--
function cfxHeloTroops.loadClosestGroup(conf)
local p = conf.unit:getPosition().p
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
unitsToLoad = cfxHeloTroops.filterTroopsByType(unitsToLoad)
-- now limit the options to the five closest legal groups
local numUnits = #unitsToLoad
if numUnits < 1 then return false end -- on false will drop through
local aTeam = unitsToLoad[1] -- get first (closest) entry
local dist = aTeam.dist
local group = aTeam.group
cfxHeloTroops.doLoadGroup({conf, group})
return true -- will have loaded and reset menu
end
function cfxHeloTroops.heloLanded(theUnit)
-- when we have landed,
if not dcsCommon.isTroopCarrier(theUnit) then return end
local conf = cfxHeloTroops.getUnitConfig(theUnit)
conf.unit = theUnit
conf.currentState = 0
-- we look if we auto-unload
if conf.autoDrop then
if conf.troopsOnBoardNum > 0 then
cfxHeloTroops.doDeployTroops({conf, "autodrop"})
-- already called set menu, can exit directly
return
end
-- when we get here, we have no troops to drop on board
-- so nothing to do really except look if we can pick up troops
-- set menu will do that for us
end
if conf.autoPickup then
if conf.troopsOnBoardNum < 1 then
-- load the closest group
if cfxHeloTroops.loadClosestGroup(conf) then
return
end
end
end
-- when we get here, we simply set the newest menus and are done
-- reset menu
cfxHeloTroops.removeComms(conf.unit)
cfxHeloTroops.setCommsMenu(conf.unit)
end
--
--
-- Helo took off
--
--
function cfxHeloTroops.heloDeparted(theUnit)
if not dcsCommon.isTroopCarrier(theUnit) 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
local conf = cfxHeloTroops.getUnitConfig(theUnit)
conf.currentState = 1 -- in the air
cfxHeloTroops.removeComms(conf.unit)
cfxHeloTroops.setCommsMenu(conf.unit)
end
--
--
-- Helo Crashed
--
--
function cfxHeloTroops.heloCrashed(theUnit)
if not dcsCommon.isTroopCarrier(theUnit) then return end
-- 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?
conf.troopsOnBoard = {}
cfxHeloTroops.removeComms(conf.unit)
end
--
--
-- M E N U H A N D L I N G & R E S P O N S E
--
--
function cfxHeloTroops.clearCommsSubmenus(conf)
if conf.myCommands then
for i=1, #conf.myCommands do
missionCommands.removeItemForGroup(conf.id, conf.myCommands[i])
end
end
conf.myCommands = {}
end
function cfxHeloTroops.removeCommsFromConfig(conf)
cfxHeloTroops.clearCommsSubmenus(conf)
if conf.myMainMenu then
missionCommands.removeItemForGroup(conf.id, conf.myMainMenu)
conf.myMainMenu = nil
end
end
function cfxHeloTroops.removeComms(theUnit)
if not theUnit then return end
if not theUnit:isExist() then return end
local group = theUnit:getGroup()
local id = group:getID()
local conf = cfxHeloTroops.getUnitConfig(theUnit)
conf.id = id
conf.unit = theUnit
cfxHeloTroops.removeCommsFromConfig(conf)
end
function cfxHeloTroops.addConfigMenu(conf)
-- we add the a menu showing current state
-- and the option to change fro auto drop
-- and auto pickup
local onOff = "OFF"
if conf.autoDrop then onOff = "ON" end
local theCommand = missionCommands.addCommandForGroup(
conf.id,
'Auto-Drop: ' .. onOff .. ' - Select to change',
conf.myMainMenu,
cfxHeloTroops.redirectToggleConfig,
{conf, "drop"}
)
table.insert(conf.myCommands, theCommand)
onOff = "OFF"
if conf.autoPickup then onOff = "ON" end
theCommand = missionCommands.addCommandForGroup(
conf.id,
'Auto-Pickup: ' .. onOff .. ' - Select to change',
conf.myMainMenu,
cfxHeloTroops.redirectToggleConfig,
{conf, "pickup"}
)
table.insert(conf.myCommands, theCommand)
end
function cfxHeloTroops.setCommsMenu(theUnit)
-- depending on own load state, we set the command structure
-- it begins at 10-other, and has 'Assault Troops' as main menu with submenus
-- as required
if not theUnit then return end
if not theUnit:isExist() then return end
-- we only add this menu to troop carriers
if not dcsCommon.isTroopCarrier(theUnit) then return end
local group = theUnit:getGroup()
local id = group:getID()
local conf = cfxHeloTroops.getUnitConfig(theUnit) --cfxHeloTroops.unitConfigs[theUnit:getName()]
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')
end
-- clear out existing commands
cfxHeloTroops.clearCommsSubmenus(conf)
-- now we have a menu without submenus.
-- add our own submenus
cfxHeloTroops.addConfigMenu(conf)
-- now see if we are on the ground or in the air
-- or unknown
if conf.currentState < 0 then
conf.currentState = 0 -- landed
if theUnit:inAir() then
conf.currentState = 1
end
end
if conf.currentState == 0 then
cfxHeloTroops.addGroundMenu(conf)
else
cfxHeloTroops.addAirborneMenu(conf)
end
end
function cfxHeloTroops.addAirborneMenu(conf)
-- while we are airborne, there isn't much to do except add a status menu that does nothing
-- but we can add some instructions
-- let's begin by assuming no troops aboard
local commandTxt = "(To load troops, land in proximity to them)"
if conf.troopsOnBoardNum > 0 then
commandTxt = "(You are carrying " .. conf.troopsOnBoardNum .. " Assault Troops. Land to deploy them"
end
local theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myMainMenu,
cfxHeloTroops.redirectNoAction,
{conf, "none"}
)
table.insert(conf.myCommands, theCommand)
end
function cfxHeloTroops.redirectNoAction(args)
-- actually, we do not redirect since there is nothing to do
end
function cfxHeloTroops.addGroundMenu(conf)
-- this is the most complex menu. Player can deploy troops when loaded
-- and load troops when they are in proximity
-- case 1: troops aboard
if conf.troopsOnBoardNum > 0 then
local theCommand = missionCommands.addCommandForGroup(
conf.id,
"Deploy Team <" .. conf.troopsOnBoard.name .. ">",
conf.myMainMenu,
cfxHeloTroops.redirectDeployTroops,
{conf, "deploy"}
)
table.insert(conf.myCommands, theCommand)
return
end
-- case 2A: no troops aboard, and spawners in range
-- that are requestable
local p = conf.unit:getPosition().p
local mySide = conf.unit:getCoalition()
if cfxSpawnZones then
-- only if SpawnZones is implemented
local availableSpawnersRaw = cfxSpawnZones.getRequestableSpawnersInRange(p, 500, mySide)
-- DONE: requestable spawners must check for troop compatibility
local availableSpawners = {}
for idx, aSpawner in pairs(availableSpawnersRaw) do
-- filter all spawners that spawn "illegal" troops
local theTypes = aSpawner.types
local typeArray = dcsCommon.splitString(theTypes, ',')
typeArray = dcsCommon.trimArray(typeArray)
local allLegal = true
for idy, aType in pairs(typeArray) do
if not dcsCommon.arrayContainsString(cfxHeloTroops.legalTroops, aType) then
allLegal = false
end
end
if allLegal then
table.insert(availableSpawners, aSpawner)
end
end
local numSpawners = #availableSpawners
if numSpawners > 5 then numSpawners = 5 end
while numSpawners > 0 do
-- for each spawner in range, create a
-- spawn menu item
local spawner = availableSpawners[numSpawners]
local theName = spawner.baseName
local comm = "Request <" .. theName .. "> troops for transport" -- .. math.floor(aTeam.dist) .. "m away"
local theCommand = missionCommands.addCommandForGroup(
conf.id,
comm,
conf.myMainMenu,
cfxHeloTroops.redirectSpawnGroup,
{conf, spawner}
)
table.insert(conf.myCommands, theCommand)
numSpawners = numSpawners - 1
end
end
-- case 2B: no troops aboard. see if there are troops around
-- that we can load up
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
-- TODO: add weight filtering
unitsToLoad = cfxHeloTroops.filterTroopsByType(unitsToLoad)
-- now limit the options to the five closest legal groups
local numUnits = #unitsToLoad
if numUnits > 5 then numUnits = 5 end
if numUnits < 1 then
local theCommand = missionCommands.addCommandForGroup(
conf.id,
"(No units in range)",
conf.myMainMenu,
cfxHeloTroops.redirectNoAction,
{conf, "none"}
)
table.insert(conf.myCommands, theCommand)
return
end
-- add an entry for each group in units to load
for i=1, numUnits do
local aTeam = unitsToLoad[i]
local dist = aTeam.dist
local group = aTeam.group
local tNum = group:getSize()
local comm = "Load <" .. group:getName() .. "> " .. tNum .. " Members" -- .. math.floor(aTeam.dist) .. "m away"
local theCommand = missionCommands.addCommandForGroup(
conf.id,
comm,
conf.myMainMenu,
cfxHeloTroops.redirectLoadGroup,
{conf, group}
)
table.insert(conf.myCommands, theCommand)
end
end
function cfxHeloTroops.filterTroopsByType(unitsToLoad)
local filteredGroups = {}
for idx, aTeam in pairs(unitsToLoad) do
local group = aTeam.group
local theTypes = dcsCommon.getGroupTypeString(group)
local aT = dcsCommon.splitString(theTypes, ",")
local pass = true
for iT, sT in pairs(aT) do
-- check if this is a valid type
if not dcsCommon.arrayContainsString(cfxHeloTroops.legalTroops, sT) then
pass = false
break
end
end
if pass then
table.insert(filteredGroups, aTeam)
end
end
return filteredGroups
end
--
-- T O G G L E S
--
function cfxHeloTroops.redirectToggleConfig(args)
timer.scheduleFunction(cfxHeloTroops.doToggleConfig, args, timer.getTime() + 0.1)
end
function cfxHeloTroops.doToggleConfig(args)
local conf = args[1]
local what = args[2]
if what == "drop" then
conf.autoDrop = not conf.autoDrop
if conf.autoDrop then
trigger.action.outTextForGroup(conf.id, "Now deploying troops immediately after landing", 30)
else
trigger.action.outTextForGroup(conf.id, "Troops will now only deploy when told to", 30)
end
else
conf.autoPickup = not conf.autoPickup
if conf.autoPickup then
trigger.action.outTextForGroup(conf.id, "Nearest troops will now automatically board after landing", 30)
else
trigger.action.outTextForGroup(conf.id, "Troops will now board only after being ordered to do so", 30)
end
end
cfxHeloTroops.setCommsMenu(conf.unit)
end
--
-- Deploying Troops
--
function cfxHeloTroops.redirectDeployTroops(args)
timer.scheduleFunction(cfxHeloTroops.doDeployTroops, args, timer.getTime() + 0.1)
end
function cfxHeloTroops.doDeployTroops(args)
local conf = args[1]
local what = args[2]
-- deploy the troops I have on board in formation
cfxHeloTroops.deployTroopsFromHelicopter(conf)
-- set own troops to 0 and erase type string
conf.troopsOnBoardNum = 0
conf.troopsOnBoard = {}
-- conf.troopsOnBoardTypes = ""
conf.troopsOnBoard.name = "***wasdeployed***"
-- reset menu
cfxHeloTroops.removeComms(conf.unit)
cfxHeloTroops.setCommsMenu(conf.unit)
end
function cfxHeloTroops.deployTroopsFromHelicopter(conf)
-- we have troops, drop them now
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
table.insert(unitTypes, "Soldier M4") -- make it one m4 trooper as fallback
end
local range = conf.troopsOnBoard.range
local orders = conf.troopsOnBoard.orders
if not orders then orders = "guard" end
-- order processing: if the orders were pre-pended with "wait-"
-- we now remove that, so after dropping they do what their
-- orders where AFTER being picked up
if dcsCommon.stringStartsWith(orders, "wait-") then
orders = dcsCommon.removePrefix(orders, "wait-")
trigger.action.outTextForGroup(conf.id, "+++ <" .. conf.troopsOnBoard.name .. "> revoke 'wait' orders, proceed with <".. orders .. ">", 30)
end
local chopperZone = cfxZones.createSimpleZone("choppa", p, 12) -- 12 m ratius around choppa
--local theCoalition = theUnit:getCountry() -- make it choppers country
local theCoalition = theUnit:getGroup():getCoalition() -- make it choppers COALITION
local theGroup = cfxZones.createGroundUnitsInZoneForCoalition (
theCoalition,
conf.troopsOnBoard.name, -- dcsCommon.uuid("Assault"), -- maybe use config name as loaded from the group
chopperZone,
unitTypes,
conf.dropFormation,
90)
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)
trigger.action.outTextForGroup(conf.id, "<" .. theGroup:getName() .. "> have deployed to the ground with orders " .. orders .. "!", 30)
end
--
-- Loading Troops
--
function cfxHeloTroops.redirectLoadGroup(args)
timer.scheduleFunction(cfxHeloTroops.doLoadGroup, args, timer.getTime() + 0.1)
end
function cfxHeloTroops.doLoadGroup(args)
local conf = args[1]
local group = args[2]
conf.troopsOnBoard = {}
-- all we need to do is disassemble the group into type
conf.troopsOnBoard.types = dcsCommon.getGroupTypeString(group)
-- get the size
conf.troopsOnBoardNum = group:getSize()
-- and name
conf.troopsOnBoard.name = group:getName()
-- and put it all into the helicopter config
-- now we need to destroy the group. First, 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
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
end
-- 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
group:destroy()
-- say so
trigger.action.outTextForGroup(conf.id, "Team '".. conf.troopsOnBoard.name .."' aboard, ready to go!", 30)
-- reset menu
cfxHeloTroops.removeComms(conf.unit)
cfxHeloTroops.setCommsMenu(conf.unit)
end
--
-- spawning troops
--
function cfxHeloTroops.redirectSpawnGroup(args)
timer.scheduleFunction(cfxHeloTroops.doSpawnGroup, args, timer.getTime() + 0.1)
end
function cfxHeloTroops.delayedCommsResetForUnit(args)
local theUnit = args[1]
cfxHeloTroops.removeComms(theUnit)
cfxHeloTroops.setCommsMenu(theUnit)
end
function cfxHeloTroops.doSpawnGroup(args)
local conf = args[1]
local theSpawner = args[2]
-- make sure cooldown on spawner has timed out, else
-- notify that you have to wait
local now = timer.getTime()
if now < (theSpawner.lastSpawnTimeStamp + theSpawner.cooldown) then
local delta = math.floor(theSpawner.lastSpawnTimeStamp + theSpawner.cooldown - now)
trigger.action.outTextForGroup(conf.id, "Still redeploying (" .. delta .. " seconds left)", 30)
return
end
cfxSpawnZones.spawnWithSpawner(theSpawner)
trigger.action.outTextForGroup(conf.id, "Deploying <" .. theSpawner.baseName .. "> now...", 30)
-- reset all comms so we can include new troops
-- into load menu
timer.scheduleFunction(cfxHeloTroops.delayedCommsResetForUnit, {conf.unit, "ignore"}, now + 1.0)
end
--
-- Player event callbacks
--
function cfxHeloTroops.playerChangeEvent(evType, description, player, data)
if evType == "newGroup" then
theUnit = data.primeUnit
cfxHeloTroops.setCommsMenu(theUnit)
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 conf then
cfxHeloTroops.removeCommsFromConfig(conf)
end
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
end
--
-- read config zone
--
function cfxHeloTroops.readConfigZone()
-- note: must match exactly!!!!
local theZone = cfxZones.getZoneByName("heloTroopsConfig")
if not theZone then
trigger.action.outText("+++heloT: no config zone!", 30)
return
end
cfxHeloTroops.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
if cfxZones.hasProperty(theZone, "legalTroops") then
local theTypesString = cfxZones.getStringFromZoneProperty(theZone, "legalTroops", "")
local unitTypes = dcsCommon.splitString(aSpawner.types, ",")
if #unitTypes < 1 then
unitTypes = {"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",} -- default
else
unitTypes = dcsCommon.trimArray(unitTypes)
end
cfxHeloTroops.legalTroops = unitTypes
end
cfxHeloTroops.troopWeight = cfxZones.getNumberFromZoneProperty(theZone, "troopWeight", 100) -- kg average weight per trooper
cfxHeloTroops.autoDrop = cfxZones.getBoolFromZoneProperty(theZone, "autoDrop", false)
cfxHeloTroops.autoPickup = cfxZones.getBoolFromZoneProperty(theZone, "autoPickup", false)
cfxHeloTroops.pickupRange = cfxZones.getNumberFromZoneProperty(theZone, "pickupRange", 100)
end
--
-- Start
--
function cfxHeloTroops.start()
-- check libs
if not dcsCommon.libCheck("cfx Helo Troops",
cfxHeloTroops.requiredLibs) then
return false
end
-- read config zone
-- 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)
trigger.action.outText("cf/x Helo Troops v" .. cfxHeloTroops.version .. " started", 30)
return true
end
-- let's get rolling
if not cfxHeloTroops.start() then
trigger.action.outText("cf/x Helo Troops aborted: missing libraries", 30)
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

99
modules/cfxMapMarkers.lua Normal file
View File

@ -0,0 +1,99 @@
cfxMapMarkers = {}
cfxMapMarkers.version = "1.0.2"
cfxMapMarkers.autostart = true
--[[--
Version History
- 1.0.1 - initial version
- 1.0.2 - coalition property processing cleanup
--]]--
-- cfxMapMarkers places Annotations for players on the F10 map
-- Annotations are derived from Zones, and their properties control who can see them
-- Any Zone with the Property "mapMarker" will put the property's content on the Map
-- Markers are shown to everyone, except when
-- - an additional Property 'coalition' is present, the Map Marker is only shown to that coalition. Possible coalition values are "ALL" (default), "RED", "BLUE", "NEUTRAL"
-- - an additional Property 'group' is present, the Map Marker is shown to the group with that name. Note that
-- 'coalition' and 'group' are mutually exclusive. Group overrides Coalition
-- you can access map markers by their zones, turn them on and off individually
-- if autostart is on, all zones are scanned for map markers and displayed according to their properties
-- NOTE: when placing a mark, this will add the 'markID' attribute to the zone table to identify the mark
--cfxMapMarkers.simpleUUID = 76543 -- a number to start. as good as any
--function cfxMapMarkers.uuid()
-- cfxMapMarkers.simpleUUID = cfxMapMarkers.simpleUUID + 1
-- return cfxMapMarkers.simpleUUID
--end
function cfxMapMarkers.addMapMarkerForZone(theZone)
local markText = cfxZones.getZoneProperty(theZone, "mapMarker")
if not markText then return end
if markText == "" then markText = "I am empty of content, devoid of meaning" end
-- if there is a map marker already, remove it
cfxMapMarkers.removeMapMarkerForZone(theZone)
-- get a new map marker ID
local markID = dcsCommon.numberUUID()
-- see if there is a group or coalition target
local coal = cfxZones.getStringFromZoneProperty(theZone, "coalition", "ALL")
if coal == "1" then coal = "RED" end
if coal == "2" then coal = "BLUE" end
coal = coal:upper()
--coal = string.upper(coal)
local toGroup = cfxZones.getZoneProperty(theZone, "group")
if toGroup then toGroup = Group.getByName(toGroup) end
if toGroup then
-- mark to group
local groupID = toGroup:getID()
trigger.action.markToGroup(markID, markText, theZone.point, groupID, true, "")
theZone.markID = markID
return
end
-- make sure we have a legal coalition
if coal ~= "BLUE" and coal ~= "RED" and coal ~= "NEUTRAL" then
coal = "ALL"
end
if coal == "ALL" then
-- place the map marker to ALL coalitions
trigger.action.markToAll(markID, markText, theZone.point, true, "")
theZone.markID = markID
return
end
-- if we get here. we should mark by coalition
local theSide = 0 -- neutral (default)
if coal == "RED" then
theSide = 1
end
if coal == "BLUE" then
theSide = 2
end
trigger.action.markToCoalition(markID, markText, theZone.point, theSide, true, "")
theZone.markID = markID
end
function cfxMapMarkers.removeMapMarkerForZone(theZone)
if theZone.markID then
trigger.action.removeMark(theZone.markID)
end
end
function cfxMapMarkers.start()
-- collect all zones that have the 'MapMarker" Attribute
local attrZones = cfxZones.getZonesWithAttributeNamed("mapMarker")
-- process every zone
for k, aZone in pairs(attrZones) do
cfxMapMarkers.addMapMarkerForZone(aZone)
end
end
if cfxMapMarkers.autostart then cfxMapMarkers.start() end
trigger.action.outText("cfx Map Markers v" .. cfxMapMarkers.version .. " started.", 30)

126
modules/cfxNDB.lua Normal file
View File

@ -0,0 +1,126 @@
cfxNDB = {}
cfxNDB.version = "1.0.0"
cfxNDB.verbose = false
cfxNDB.ups = 10 -- every 10 seconds
cfxNDB.requiredLibs = {
"dcsCommon",
"cfxZones",
}
cfxNDB.refresh = 10 -- for moving ndb: interval in secs between refresh
cfxNDB.power = 100
cfxNDB.ndbs = {} -- all ndbs
--
-- NDB zone - *** EXTENDS ZONES ***
--
function cfxNDB.startNDB(theNDB)
theNDB.ndbRefreshTime = timer.getTime() + theNDB.ndbRefresh -- only used in linkedUnit, but set up anyway
-- generate new ID
theNDB.ndbID = dcsCommon.uuid("ndb")
local fileName = "l10n/DEFAULT/" .. theNDB.ndbSound -- need to prepend the resource string
local modulation = 0
if theNDB.fm then modulation = 1 end
local loc = cfxZones.getPoint(theNDB)
trigger.action.radioTransmission(fileName, loc, modulation, true, theNDB.freq, theNDB.power, theNDB.ndbID)
if cfxNDB.verbose then
local dsc = ""
if theNDB.linkedUnit then
dsc = " (linked to ".. theNDB.linkedUnit:getName() .. "!, r=" .. theNDB.ndbRefresh .. ") "
end
trigger.action.outText("+++ndb: started " .. theNDB.name .. dsc .. " at " .. theNDB.freq/1000000 .. "mod " .. modulation .. " with w=" .. theNDB.power .. " s=<" .. fileName .. ">", 30)
end
end
function cfxNDB.stopNDB(theNDB)
trigger.action.stopRadioTransmission(theNDB.ndbID)
end
function cfxNDB.createNDBWithZone(theZone)
theZone.freq = cfxZones.getNumberFromZoneProperty(theZone, "NDB", 124) -- in MHz
-- convert MHz to Hz
theZone.freq = theZone.freq * 1000000 -- Hz
theZone.fm = cfxZones.getBoolFromZoneProperty(theZone, "fm", false)
theZone.ndbSound = cfxZones.getStringFromZoneProperty(theZone, "soundFile", "<none>")
theZone.power = cfxZones.getNumberFromZoneProperty(theZone, "watts", cfxNDB.power)
theZone.loop = true -- always. NDB always loops
-- UNSUPPORTED refresh. Although read individually, it only works
-- when LARGER than module's refresh.
theZone.ndbRefresh = cfxZones.getNumberFromZoneProperty(theZone, "ndbRefresh", cfxNDB.refresh) -- only used if linked
theZone.ndbRefreshTime = timer.getTime() + theZone.ndbRefresh -- only used with linkedUnit, but set up nonetheless
-- start it
cfxNDB.startNDB(theZone)
-- add it to my watchlist
table.insert(cfxNDB.ndbs, theZone)
end
--
-- update
--
function cfxNDB.update()
timer.scheduleFunction(cfxNDB.update, {}, timer.getTime() + 1/cfxNDB.ups)
local now = timer.getTime()
-- walk through all NDB and see if they need a refresh
for idx, theNDB in pairs (cfxNDB.ndbs) do
-- see if this ndb is linked, meaning it's potentially
-- moving with the linked unit
if theNDB.linkedUnit then
-- yupp, need to update
if now > theNDB.ndbRefreshTime then
cfxNDB.stopNDB(theNDB)
cfxNDB.startNDB(theNDB)
end
end
end
end
--
-- start up
--
function cfxNDB.readConfig()
local theZone = cfxZones.getZoneByName("ndbConfig")
if not theZone then
if cfxNDB.verbose then
trigger.action.outText("***ndb: NO config zone!", 30)
end
return
end
cfxNDB.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
cfxNDB.ndbRefresh = cfxZones.getNumberFromZoneProperty(theZone, "ndbRefresh", 10)
if cfxNDB.verbose then
trigger.action.outText("***ndb: read config", 30)
end
end
function cfxNDB.start()
-- lib check
if not dcsCommon.libCheck("cfx NDB",
cfxNDB.requiredLibs) then
return false
end
-- config
cfxNDB.readConfig()
-- read zones
local attrZones = cfxZones.getZonesWithAttributeNamed("NDB")
for idx, aZone in pairs(attrZones) do
cfxNDB.createNDBWithZone(aZone)
end
-- start update
cfxNDB.update()
return true
end
if not cfxNDB.start() then
trigger.action.outText("cf/x NDB aborted: missing libraries", 30)
cfxNDB = nil
end

View File

@ -0,0 +1,150 @@
cfxObjectDestructDetector = {}
cfxObjectDestructDetector.version = "1.0.1"
cfxObjectDestructDetector.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
}
cfxObjectDestructDetector.verbose = false
--[[--
VERSION HISTORY
1.0.0 initial version, based on parashoo, arty zones
1.0.1 fixed bug: trigger.MISC.getUserFlag()
Detect when an object with OBJECT ID as assigned in ME dies
*** EXTENDS ZONES
--]]--
cfxObjectDestructDetector.objectZones = {}
--
-- C A L L B A C K S
--
cfxObjectDestructDetector.callbacks = {}
function cfxObjectDestructDetector.addCallback(theCallback)
table.insert(cfxObjectDestructDetector.callbacks, theCallback)
end
function cfxObjectDestructDetector.invokeCallbacksFor(zone)
for idx, theCB in pairs (cfxObjectDestructDetector.callbacks) do
theCB(zone, zone.ID, zone.name)
end
end
--
-- zone handling
--
function cfxObjectDestructDetector.addObjectDetectZone(aZone)
-- add landHeight to this zone
table.insert(cfxObjectDestructDetector.objectZones, aZone)
end
--
-- processing of zones
--
function cfxObjectDestructDetector.processObjectDestructZone(aZone)
aZone.name = cfxZones.getStringFromZoneProperty(aZone, "NAME", aZone.name)
-- aZone.coalition = cfxZones.getCoalitionFromZoneProperty(aZone, "coalition", 0)
aZone.ID = cfxZones.getNumberFromZoneProperty(aZone, "OBJECT ID", 1) -- THIS!
if cfxZones.hasProperty(aZone, "setFlag") then
aZone.setFlag = cfxZones.getStringFromZoneProperty(aZone, "setFlag", "999")
end
if cfxZones.hasProperty(aZone, "f=1") then
aZone.setFlag = cfxZones.getStringFromZoneProperty(aZone, "f=1", "999")
end
if cfxZones.hasProperty(aZone, "clearFlag") then
aZone.clearFlag = cfxZones.getStringFromZoneProperty(aZone, "clearFlag", "999")
end
if cfxZones.hasProperty(aZone, "f=0") then
aZone.clearFlag = cfxZones.getStringFromZoneProperty(aZone, "f=0", "999")
end
if cfxZones.hasProperty(aZone, "increaseFlag") then
aZone.increaseFlag = cfxZones.getStringFromZoneProperty(aZone, "increaseFlag", "999")
end
if cfxZones.hasProperty(aZone, "f+1") then
aZone.increaseFlag = cfxZones.getStringFromZoneProperty(aZone, "f+1", "999")
end
if cfxZones.hasProperty(aZone, "decreaseFlag") then
aZone.decreaseFlag = cfxZones.getStringFromZoneProperty(aZone, "decreaseFlag", "999")
end
if cfxZones.hasProperty(aZone, "f-1") then
aZone.decreaseFlag = cfxZones.getStringFromZoneProperty(aZone, "f-1", "999")
end
end
--
-- MAIN DETECTOR
--
-- invoke callbacks when an object was destroyed
function cfxObjectDestructDetector:onEvent(event)
if event.id == world.event.S_EVENT_DEAD then
if not event.initiator then return end
local id = event.initiator:getName()
if not id then return end
for idx, aZone in pairs(cfxObjectDestructDetector.objectZones) do
if aZone.ID == id then
-- flag manipulation
if aZone.setFlag then
trigger.action.setUserFlag(aZone.setFlag, 1)
end
if aZone.clearFlag then
trigger.action.setUserFlag(aZone.clearFlag, 0)
end
if aZone.increaseFlag then
local val = trigger.misc.getUserFlag(aZone.increaseFlag) + 1
trigger.action.setUserFlag(aZone.increaseFlag, val)
end
if aZone.decreaseFlag then
local val = trigger.misc.getUserFlag(aZone.decreaseFlag) - 1
trigger.action.setUserFlag(aZone.decreaseFlag, val)
end
-- invoke callbacks
cfxObjectDestructDetector.invokeCallbacksFor(aZone)
if cfxObjectDestructDetector.verbose then
trigger.action.outText("OBJECT KILL: " .. id, 30)
end
-- we could now remove the object from the list
-- for better performance since it cant
-- die twice
return
end
end
end
end
-- add event handler
function cfxObjectDestructDetector.start()
if not dcsCommon.libCheck("cfx Object Destruct Detector",
cfxObjectDestructDetector.requiredLibs) then
return false
end
-- collect all zones with 'smoke' attribute
-- collect all spawn zones
local attrZones = cfxZones.getZonesWithAttributeNamed("OBJECT ID")
-- now create a spawner for all, add them to the spawner updater, and spawn for all zones that are not
-- paused
for k, aZone in pairs(attrZones) do
cfxObjectDestructDetector.processObjectDestructZone(aZone) -- process attribute and add to zone properties (extend zone)
cfxObjectDestructDetector.addObjectDetectZone(aZone) -- remember it so we can smoke it
end
-- add myself as event handler
world.addEventHandler(cfxObjectDestructDetector)
-- say hi
trigger.action.outText("cfx Object Destruct Zones v" .. cfxObjectDestructDetector.version .. " started.", 30)
return true
end
-- let's go
if not cfxObjectDestructDetector.start() then
trigger.action.outText("cf/x Object Destruct Zones aborted: missing libraries", 30)
cfxObjectDestructDetector = nil
end

View File

@ -0,0 +1,481 @@
cfxObjectSpawnZones = {}
cfxObjectSpawnZones.version = "1.1.3"
cfxObjectSpawnZones.requiredLibs = {
"dcsCommon", -- common is of course needed for everything
-- pretty stupid to check for this since we
-- need common to invoke the check, but anyway
"cfxZones", -- Zones, of course. MUST HAVE RUN
}
cfxObjectSpawnZones.ups = 1
--
-- Zones that conform with this requirements spawn toops automatically
-- *** DOES NOT EXTEND ZONES ***
--
-- version history
-- 1.0.0 - based on 1.4.6 version from cfxSpawnZones
-- 1.1.0 - uses linkedUnit, so spawnming can occur on ships
-- make sure you also enable useOffset to place the
-- statics away from the center of the ship
-- 1.1.1 - also processes paused flag
-- - despawnRemaining(spawner)
-- 1.1.2 - autoRemove option re-installed
-- - added possibility to autoUnlink
-- 1.1.3 - ME-triggered flag via f? and triggerFlag
-- Object spawn zones have the following major uses:
-- - dynamically spawn cargo
-- - litter a zone with static obejcts quickly
-- - linking static dynamic objects with ships, including cargo
--
--
-- How do we recognize an object spawn zone?
-- contains a "objectSpawner" attribute
-- a spawner must also have the following attributes
-- - objectSpawner - anything, must be present to signal. put in 'ground' to be able to expand to other types
-- - types - type strings, comma separated. They all spawn in the same spot.
-- see here: https://github.com/mrSkortch/DCS-miscScripts/tree/master/ObjectDB
-- - count - repeat types n times. 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
-- - baseName - MANDATORY, for naming spawned objects - MUST BE UNIQUE!!!!
-- - heading in DEGREES (default 0 = north ) direction entire group is facing
-- - weight - weight in kg if transportable
-- - isCargo - boolean yes means it can be carried by other units
-- - cooldown - seconds to cool down before re-spawning
-- - maxSpawns - how many times we spawn. default = 1, -1 = unlimited
-- - requestable -- via comms menu, will auto-pause
-- - managed -- if cargo, it's automatically passed to cfx cargo manager for handling (if cargo manager is loaded)
-- - (linkedUnit) for placing on ships
-- - (useOffset) for not using ship's center
-- respawn currently happens after theSpawns is deleted and cooldown seconds have passed
cfxObjectSpawnZones.allSpawners = {}
cfxObjectSpawnZones.callbacks = {} -- signature: cb(reason, group, spawner)
--
-- C A L L B A C K S
--
function cfxObjectSpawnZones.addCallback(theCallback)
table.insert(cfxObjectSpawnZones.callbacks, theCallback)
end
function cfxObjectSpawnZones.invokeCallbacksFor(reason, theSpawns, theSpawner)
for idx, theCB in pairs (cfxObjectSpawnZones.callbacks) do
theCB(reason, theSpawns, theSpawner)
end
end
--
-- creating a spawner
--
function cfxObjectSpawnZones.createSpawner(inZone)
local theSpawner = {}
theSpawner.zone = inZone
theSpawner.name = inZone.name
-- connect with ME if a trigger flag is given
if cfxZones.hasProperty(inZone, "f?") then
theSpawner.triggerFlag = cfxZones.getStringFromZoneProperty(inZone, "f?", "none")
theSpawner.lastTriggerValue = trigger.misc.getUserFlag(theSpawner.triggerFlag)
end
--theSpawner.types = cfxZones.getZoneProperty(inZone, "types")
theSpawner.types = cfxZones.getStringFromZoneProperty(inZone, "types", "White_Tyre")
local n = cfxZones.getNumberFromZoneProperty(inZone, "count", 1) -- DO NOT CONFUSE WITH OWN PROPERTY COUNT for unique names!!!
if n < 1 then n = 1 end -- sanity check.
theSpawner.numObj = n
theSpawner.country = cfxZones.getNumberFromZoneProperty(inZone, "country", 2) -- coalition2county(theSpawner.owner)
theSpawner.rawOwner = coalition.getCountryCoalition(theSpawner.country)
theSpawner.baseName = cfxZones.getStringFromZoneProperty(inZone, "baseName", dcsCommon.uuid("objSpwn"))
--cfxZones.getZoneProperty(inZone, "baseName")
theSpawner.cooldown = cfxZones.getNumberFromZoneProperty(inZone, "cooldown", 60)
theSpawner.lastSpawnTimeStamp = -10000 -- just init so it will always work
theSpawner.autoRemove = cfxZones.getBoolFromZoneProperty(inZone, "autoRemove", false)
theSpawner.autoLink = cfxZones.getBoolFromZoneProperty(inZone, "autoLink", true)
theSpawner.heading = cfxZones.getNumberFromZoneProperty(inZone, "heading", 0)
theSpawner.weight = cfxZones.getNumberFromZoneProperty(inZone, "weight", 0)
if theSpawner.weight < 0 then theSpawner.weight = 0 end
theSpawner.isCargo = cfxZones.getBoolFromZoneProperty(inZone, "isCargo", false)
if theSpawner.isCargo == true and theSpawner.weight < 100 then theSpawner.weight = 100 end
theSpawner.managed = cfxZones.getBoolFromZoneProperty(inZone, "managed", true) -- defaults to managed cargo
theSpawner.cdTimer = 0 -- used for cooldown. if timer.getTime < this value, don't spawn
-- theSpawner.cdStarted = false -- used to initiate cooldown when all items in theSpawns disappear
theSpawner.count = 1 -- used to create names, and count how many groups created
theSpawner.theSpawns = {} -- all items that are spawned. re-spawn happens if they are all out
theSpawner.maxSpawns = cfxZones.getNumberFromZoneProperty(inZone, "maxSpawns", 1)
theSpawner.paused = cfxZones.getBoolFromZoneProperty(inZone, "paused", false)
theSpawner.requestable = cfxZones.getBoolFromZoneProperty(inZone, "requestable", false)
if theSpawner.requestable then theSpawner.paused = true end
-- see if it is linked to a ship to set realtive orig headiong
if inZone.linkedUnit then
local shipUnit = inZone.linkedUnit
theSpawner.linkedUnit = shipUnit
--trigger.action.outText("+++obSpZ: zone " .. inZone.name .. " linked to ship (?) " .. shipUnit:getName() .. " of cat " .. shipUnit:getCategory(), 30)
local origHeading = dcsCommon.getUnitHeadingDegrees(shipUnit)
-- calculate initial delta
local delta = dcsCommon.vSub(inZone.point,shipUnit:getPoint()) -- delta = B - A
theSpawner.dx = delta.x
theSpawner.dy = delta.z
--theSpawner.dx = inZone.dx
--theSpawner.dy = inZone.dy
theSpawner.origHeading = origHeading
--trigger.action.outText("+++obSpZ: with dx = " .. theSpawner.dx .. " dy = " .. theSpawner.dy .. " hdg = " .. origHeading, 30)
end
return theSpawner
end
function cfxObjectSpawnZones.addSpawner(aSpawner)
cfxObjectSpawnZones.allSpawners[aSpawner.zone] = aSpawner
end
function cfxObjectSpawnZones.removeSpawner(aSpawner)
cfxObjectSpawnZones.allSpawners[aSpawner.zone] = nil
end
function cfxObjectSpawnZones.getSpawnerForZone(aZone)
return cfxObjectSpawnZones.allSpawners[aZone]
end
function cfxObjectSpawnZones.getSpawnerForZoneNamed(aName)
local aZone = cfxZones.getZoneByName(aName)
return cfxObjectSpawnZones.getSpawnerForZone(aZone)
end
function cfxObjectSpawnZones.getRequestableSpawnersInRange(aPoint, aRange, aSide)
-- trigger.action.outText("enter requestable spawners for side " .. aSide , 30)
if not aSide then aSide = 0 end
-- currently, WE FORCE A SIDE MATCH
aSide = 0
if not aRange then aRange = 200 end
if not aPoint then return {} end
local theSpawners = {}
for aZone, aSpawner in pairs(cfxObjectSpawnZones.allSpawners) do
-- iterate all zones and collect those that match
local hasMatch = true
-- update the zone's point if it is linked to a ship, i.e.
local delta = dcsCommon.dist(aPoint, cfxZones.getPoint(aZone))
if delta>aRange then hasMatch = false end
if aSide ~= 0 then
--[[--
-- check if side is correct for owned zone
--]]--
end
--[[--
if aSide ~= aSpawner.rawOwner then
-- only return spawners with this side
-- note: this will NOT work with neutral players
hasMatch = false
end
--]]--
if not aSpawner.requestable then
hasMatch = false
end
if hasMatch then
table.insert(theSpawners, aSpawner)
end
end
return theSpawners
end
--
-- spawn troops
--
function cfxObjectSpawnZones.verifySpawnOwnership(spawner)
--[[--
-- returns false ONLY if masterSpawn disagrees
--]]--
return true
end
function cfxObjectSpawnZones.spawnObjectNTimes(aSpawner, theType, n, container)
if not aSpawner then return end
if not container then container = {} end
if not n then n = 1 end
if not theType then return end
local aZone = aSpawner.zone
if not aZone then return end
if n < 1 then return end
local center = cfxZones.getPoint(aZone) -- magically works with moving zones and offset
if n == 1 then
-- spawn in the middle, only a single object
local ox = center.x
local oy = center.z
local theStaticData = dcsCommon.createStaticObjectData(
aSpawner.baseName .. "-" .. aSpawner.count,
theType,
aSpawner.heading,
false, -- dead?
aSpawner.isCargo,
aSpawner.weight)
aSpawner.count = aSpawner.count + 1
-- more to global position
dcsCommon.moveStaticDataTo(theStaticData, ox, oy)
-- if linked, relative-link instead to ship
-- NOTE: it is possible that we have to re-calc heading
-- if ship turns relative to original designation position.
if aZone.linkedUnit and aZone.autoLink then
dcsCommon.linkStaticDataToUnit(
theStaticData,
aZone.linkedUnit,
aSpawner.dx,
aSpawner.dy,
aSpawner.origHeading)
end
-- spawn in dcs
local theObject = coalition.addStaticObject(aSpawner.rawOwner, theStaticData) -- create in dcs
table.insert(container, theObject) -- add to collection
if aSpawner.isCargo and aSpawner.managed then
if cfxCargoManager then
cfxCargoManager.addCargo(theObject)
end
end
return
end
local numObjects = n
local degrees = 3.14157 / 180
local degreeIncrement = (360 / numObjects) * degrees
local currDegree = 0
local missionObjects = {}
for i=1, numObjects do
local rx = math.cos(currDegree) * aZone.radius
local ry = math.sin(currDegree) * aZone.radius
local ox = center.x + rx
local oy = center.z + ry -- note: z!
local theStaticData = dcsCommon.createStaticObjectData(
aSpawner.baseName .. "-" .. aSpawner.count,
theType,
aSpawner.heading,
false, -- dead?
aSpawner.isCargo,
aSpawner.weight)
theStaticData.canCargo = aSpawner.isCargo -- should be false, but you never know
if theStaticData.canCargo then
-- theStaticData.mass = aSpawner.weight
trigger.action.outText("+++ obSpw is cargo with w=" .. theStaticData.mass .. " for " .. theStaticData.name, 30)
end
aSpawner.count = aSpawner.count + 1
dcsCommon.moveStaticDataTo(theStaticData, ox, oy)
if aZone.linkedUnit and aZone.autoLink then
dcsCommon.linkStaticDataToUnit(theStaticData, aZone.linkedUnit, aSpawner.dx + rx, aSpawner.dy + ry, aSpawner.origHeading)
end
-- spawn in dcs
local theObject = coalition.addStaticObject(aSpawner.rawOwner, theStaticData) -- this will generate an event!
table.insert(container, theObject)
-- see if it is managed cargo
if aSpawner.isCargo and aSpawner.managed then
if cfxCargoManager then
cfxCargoManager.addCargo(theObject)
end
end
-- update rotation
currDegree = currDegree + degreeIncrement
end
end
function cfxObjectSpawnZones.spawnWithSpawner(aSpawner)
if type(aSpawner) == "string" then -- return spawner for zone of that name
aSpawner = cfxObjectSpawnZones.getSpawnerForZoneNamed(aName)
end
if not aSpawner then return end
-- will NOT check if conditions are met. This forces a spawn
local unitTypes = {} -- build type names
local p = cfxZones.getPoint(aSpawner.zone) -- aSpawner.zone.point
-- split the conf.troopsOnBoardTypes into an array of types
unitTypes = dcsCommon.splitString(aSpawner.types, ",")
if #unitTypes < 1 then
table.insert(unitTypes, "White_Flag") -- make it one m4 trooper as fallback
end
-- now iterate through all types and create objects for each name
-- overlaying them all n times
aSpawner.theSpawns = {} -- forget whatever there was before.
for idx, typeName in pairs(unitTypes) do
cfxObjectSpawnZones.spawnObjectNTimes(
aSpawner,
typeName,
aSpawner.numObj,
aSpawner.theSpawns)
end
-- reset cooldown (forced)
if true then -- or not spawner.cdStarted then -- forced on
-- no, start cooldown
--spawner.cdStarted = true
aSpawner.cdTimer = timer.getTime() + aSpawner.cooldown
end
-- callback to all who want to know
cfxObjectSpawnZones.invokeCallbacksFor("spawned", aSpawner.theSpawns, aSpawner)
-- timestamp so we can check against cooldown on manual spawn
aSpawner.lastSpawnTimeStamp = timer.getTime()
-- make sure a requestable spawner is always paused
if aSpawner.requestable then
aSpawner.paused = true
end
if aSpawner.autoRemove then
-- simply remove the group
aSpawner.theSpawns = {} -- empty group -- forget all
end
end
function cfxObjectSpawnZones.despawnRemaining(spawner)
for idx, anObject in pairs (spawner.theSpawns) do
if anObject and anObject:isExist() then
anObject:destroy()
end
end
end
--
-- U P D A T E
--
function cfxObjectSpawnZones.needsSpawning(spawner)
if spawner.paused then return false end
if spawner.requestable then return false end
if spawner.maxSpawns == 0 then return false end
if #spawner.theSpawns > 0 then return false end
if timer.getTime() < spawner.cdTimer then return false end
return cfxObjectSpawnZones.verifySpawnOwnership(spawner)
end
function cfxObjectSpawnZones.update()
cfxObjectSpawnZones.updateSchedule = timer.scheduleFunction(cfxObjectSpawnZones.update, {}, timer.getTime() + 1/cfxObjectSpawnZones.ups)
for key, spawner in pairs (cfxObjectSpawnZones.allSpawners) do
-- see if the spawn is dead or was removed
-- forget all dead spawns
local objectsToKeep = {}
-- if not spawner.requestable then
for idx, anObject in pairs (spawner.theSpawns) do
if not anObject:isExist() then
--trigger.action.outText("+++ obSpwn: INEXIST removing object in zone " .. spawner.zone.name, 30)
elseif anObject:getLife() < 1 then
--trigger.action.outText("+++ obSpwn: dead. removing object ".. anObject:getName() .." in zone " .. spawner.zone.name, 30)
else
table.insert(objectsToKeep, anObject)
end
end
-- end
-- see if we killed off all objects and start cd if so
if #objectsToKeep == 0 and #spawner.theSpawns > 0 then
spawner.cdTimer = timer.getTime() + spawner.cooldown
end
-- transfer kept items
spawner.theSpawns = objectsToKeep
local needsSpawn = cfxObjectSpawnZones.needsSpawning(spawner)
-- check if perhaps our watchtrigger causes spawn
if spawner.triggerFlag then
local currTriggerVal = trigger.misc.getUserFlag(spawner.triggerFlag)
if currTriggerVal ~= spawner.lastTriggerValue then
needsSpawn = true
spawner.lastTriggerValue = currTriggerVal
end
end
if needsSpawn then
cfxObjectSpawnZones.spawnWithSpawner(spawner)
if spawner.maxSpawns > 0 then
spawner.maxSpawns = spawner.maxSpawns - 1
end
if spawner.maxSpawns == 0 then
spawner.paused = true
end
else
-- trigger.action.outText("+++ NOSPAWN for zone " .. spawner.zone.name, 30)
end
end
end
function cfxObjectSpawnZones.start()
if not dcsCommon.libCheck("cfx Object Spawn Zones",
cfxObjectSpawnZones.requiredLibs) then
return false
end
-- collect all spawn zones
local attrZones = cfxZones.getZonesWithAttributeNamed("objectSpawner")
-- now create a spawner for all, add them to the spawner updater, and spawn for all zones that are not
-- paused
for k, aZone in pairs(attrZones) do
local aSpawner = cfxObjectSpawnZones.createSpawner(aZone)
cfxObjectSpawnZones.addSpawner(aSpawner)
if (not aSpawner.paused) and
cfxObjectSpawnZones.verifySpawnOwnership(aSpawner) and
aSpawner.maxSpawns ~= 0
then
cfxObjectSpawnZones.spawnWithSpawner(aSpawner)
-- update spawn count and make sure we haven't spawned the one and only
if aSpawner.maxSpawns > 0 then
aSpawner.maxSpawns = aSpawner.maxSpawns - 1
end
if aSpawner.maxSpawns == 0 then
aSpawner.paused = true
--trigger.action.outText("+++ maxspawn -- turning off zone " .. aSpawner.zone.name, 30)
end
end
end
-- and start the regular update calls
cfxObjectSpawnZones.update()
trigger.action.outText("cfx Object Spawn Zones v" .. cfxObjectSpawnZones.version .. " started.", 30)
return true
end
if not cfxObjectSpawnZones.start() then
trigger.action.outText("cf/x Spawn Zones aborted: missing libraries", 30)
cfxObjectSpawnZones = nil
end
--[[--
IMPROVEMENTS
'formations' - concentric, pile, random, array etc.
--]]--

860
modules/cfxOwnedZones.lua Normal file
View File

@ -0,0 +1,860 @@
cfxOwnedZones = {}
cfxOwnedZones.version = "1.1.0"
cfxOwnedZones.verbose = false
cfxOwnedZones.announcer = true
--[[-- VERSION HISTORY
1.0.3 - added getNearestFriendlyZone
- added getNearestOwnedZone
- added hasOwnedZones
- added getNearestOwnedZoneToPoint
1.0.4 - changed addOwnedZone code to use cfxZone.getCoalitionFromZoneProperty
- changed to use dcsCommon.coalition2county
- changed to using correct coalition for spawing attackers and defenders
1.0.5 - repairing defenders switches to country instead coalition when calling createGroundUnitsInZoneForCoalition -- fixed
1.0.6 - removed call to conqTemplate
- verified that pause will also apply to init
- unbeatable zones
- untargetable zones
- hidden attribute
1.0.7 - optional cfxGroundTroops module, error message when attackers
- support of 'none' type string to indicate no attackers/defenders
- updated property access
- module check
- cfxOwnedTroop.usesDefenders(aZone)
- verifyZone
1.0.8 - repairDefenders trims types to allow blanks in
type separator
1.1.0 - config zone
- bang! support r, b, n capture
- defaulting attackDelta to 10 instead of radius
- verbose code for spawning
- verbose code for state transition
- attackers have (A) in name, defenders (D)
- exit createDefenders if no troops
- exit createAttackers if no troops
- usesAttackers/usesDefenders checks for neutral ownership
- verbose state change
- nearestZone supports moving zones
- remove exiting defenders from zone after cap to avoid
shocked state
- announcer
--]]--
cfxOwnedZones.requiredLibs = {
"dcsCommon", -- common is of course needed for everything
-- pretty stupid to check for this since we
-- need common to invoke the check, but anyway
"cfxZones", -- Zones, of course
"cfxCommander", -- to make troops do stuff
-- "cfxGroundTroops", -- optional, used for attackers only
}
cfxOwnedZones.zones = {}
cfxOwnedZones.ups = 1
cfxOwnedZones.initialized = false
cfxOwnedZones.defendingTime = 100 -- 100 seconds until new defenders are produced
cfxOwnedZones.attackingTime = 300 -- 300 seconds until new attackers are produced
cfxOwnedZones.shockTime = 200 -- 200 -- 'shocked' period of inactivity
cfxOwnedZones.repairTime = 200 -- 200 -- time until we raplace one lost unit, also repairs all other units to 100%
-- owned zones is a module that managers 'conquerable' zones and keeps a
-- record of who owns the zone
-- based on some simple rules that are regularly checked
--
-- *** EXTENTDS ZONES ***, so compatible with cfxZones, pilotSafe (limited airframes), may conflict with FARPZones
--
-- owned zones are identified by the 'owner' property. It can be initially set to nothing (default), NEUTRAL, RED or BLUE
-- when a zone changes hands, a callback can be installed to be told of that fact
-- callback has the format (zone, newOwner, formerOwner) with zone being the Zone, and new owner and former owners
cfxOwnedZones.conqueredCallbacks = {}
--
-- zone attributes when owned
-- owner: coalition that owns the zone
-- status: FSM for spawning
-- defendersRED/BLUE - coma separated type string for the group to spawm on defense cycle completion
-- attackersRED/BLUE - as above for attack cycle.
-- timeStamp - time when zone switched into current state
-- spawnRadius - overrides zone's radius when placing defenders. can be use to place defenders inside or outside zone itself
-- formation - defender's formation
-- attackFormation - attackers formation
-- attackRadius - radius of circle in which attackers are spawned. informs formation
-- attackDelta - polar coord: r from zone center where attackers are spawned
-- attackPhi - polar degrees where attackers are to be spawned
-- paused - will not spawn. default is false
-- unbeatable - can't be conquered by other side. default is false
-- untargetable - will not be targeted by either side. make unbeatable
-- owned zones untargetable, or they'll become a troop magnet for
-- zoneAttackers
-- hidden - if set (default no), it no markings on the map
--
-- to create an owned zone that can't be conquered and does nothing
-- add the following properties to a zone
-- owner = <x>, paused = true, unbeatable = true
--
-- callback handling
--
function cfxOwnedZones.addCallBack(conqCallback)
local cb = {}
cb.callback = conqCallback -- we use this so we can add more data later
cfxOwnedZones.conqueredCallbacks[conqCallback] = cb
end
function cfxOwnedZones.invokeConqueredCallbacks(aZone, newOwner, lastOwner)
for key, cb in pairs (cfxOwnedZones.conqueredCallbacks) do
cb.aZone = aZone -- set these up for if we need them later
cb.newOwner = newOwner
cb.lastOwner = lastOwner
-- invoke callback
cb.callback(aZone, newOwner, lastOwner)
end
end
function cfxOwnedZones.side2name(theSide)
if theSide == 1 then return "REDFORCE" end
if theSide == 2 then return "BLUEFORCE" end
return "Neutral"
end
function cfxOwnedZones.conqTemplate(aZone, newOwner, lastOwner)
if true then return end -- do not output
if lastOwner == 0 then
trigger.action.outText(cfxOwnedZones.side2name(newOwner) .. " have taken possession zone " .. aZone.name, 30)
return
end
trigger.action.outText("Zone " .. aZone.name .. " was taken by ".. cfxOwnedZones.side2name(newOwner) .. " from " .. cfxOwnedZones.side2name(lastOwner), 30)
end
--
-- M I S C
--
function cfxOwnedZones.drawZoneInMap(aZone)
-- will save markID in zone's markID
if aZone.markID then
trigger.action.removeMark(aZone.markID)
end
if aZone.hidden then return end
local lineColor = {1.0, 0, 0, 1.0} -- red
local fillColor = {1.0, 0, 0, 0.2} -- red
local owner = cfxOwnedZones.getOwnerForZone(aZone)
if owner == 2 then
lineColor = {0.0, 0, 1.0, 1.0}
fillColor = {0.0, 0, 1.0, 0.2}
elseif owner == 0 then
lineColor = {0.8, 0.8, 0.8, 1.0}
fillColor = {0.8, 0.8, 0.8, 0.2}
end
local theShape = 2 -- circle
local markID = dcsCommon.numberUUID()
trigger.action.circleToAll(-1, markID, aZone.point, aZone.radius, lineColor, fillColor, 1, true, "")
aZone.markID = markID
end
function cfxOwnedZones.addOwnedZone(aZone)
local owner = cfxZones.getCoalitionFromZoneProperty(aZone, "owner", 0) -- is already readm read it again
aZone.owner = owner -- add this attribute to zone
-- now init all other owned zone properties
aZone.state = "init"
aZone.timeStamp = timer.getTime()
--aZone.defendersRED = "Soldier M4,Soldier M4,Soldier M4,Soldier M4,Soldier M4" -- vehicles allocated to defend when red
aZone.defendersRED = cfxZones.getStringFromZoneProperty(aZone, "defendersRED", "none")
aZone.defendersBLUE = cfxZones.getStringFromZoneProperty(aZone, "defendersBLUE", "none")
aZone.attackersRED = cfxZones.getStringFromZoneProperty(aZone, "attackersRED", "none")
aZone.attackersBLUE = cfxZones.getStringFromZoneProperty(aZone, "attackersBLUE", "none")
local formation = cfxZones.getZoneProperty(aZone, "formation")
if not formation then formation = "circle_out" end
aZone.formation = formation
formation = cfxZones.getZoneProperty(aZone, "attackFormation")
if not formation then formation = "circle_out" end
aZone.attackFormation = formation
local spawnRadius = cfxZones.getNumberFromZoneProperty(aZone, "spawnRadius", aZone.radius-5) -- "-5" so they remaininside radius
aZone.spawnRadius = spawnRadius
local attackRadius = cfxZones.getNumberFromZoneProperty(aZone, "attackRadius", aZone.radius)
aZone.attackRadius = attackRadius
local attackDelta = cfxZones.getNumberFromZoneProperty(aZone, "attackDelta", 10) -- aZone.radius)
aZone.attackDelta = attackDelta
local attackPhi = cfxZones.getNumberFromZoneProperty(aZone, "attackPhi", 0)
aZone.attackPhi = attackPhi
local paused = cfxZones.getBoolFromZoneProperty(aZone, "paused", false)
aZone.paused = paused
aZone.unbeatable = cfxZones.getBoolFromZoneProperty(aZone, "unbeatable", false)
aZone.untargetable = cfxZones.getBoolFromZoneProperty(aZone, "untargetable", false)
aZone.hidden = cfxZones.getBoolFromZoneProperty(aZone, "hidden", false)
cfxOwnedZones.zones[aZone] = aZone
cfxOwnedZones.drawZoneInMap(aZone)
cfxOwnedZones.verifyZone(aZone)
end
function cfxOwnedZones.verifyZone(aZone)
-- do some sanity checks
if not cfxGroundTroops and (aZone.attackersRED ~= "none" or aZone.attackersBLUE ~= "none") then
trigger.action.outText("+++owdZ: " .. aZone.name .. " attackers need cfxGroundTroops to function")
end
end
function cfxOwnedZones.getOwnerForZone(aZone)
local theZone = cfxOwnedZones.zones[aZone]
if not theZone then return 0 end -- unknown zone, return neutral as default
return theZone.owner
end
function cfxOwnedZones.getEnemyZonesFor(aCoalition)
local enemyZones = {}
local ourEnemy = dcsCommon.getEnemyCoalitionFor(aCoalition)
for zKey, aZone in pairs(cfxOwnedZones.zones) do
if aZone.owner == ourEnemy then -- only check enemy owned zones
-- note: will include untargetable zones
table.insert(enemyZones, aZone)
end
end
return enemyZones
end
function cfxOwnedZones.getNearestOwnedZoneToPoint(aPoint)
local shortestDist = math.huge
local closestZone = nil
for zKey, aZone in pairs(cfxOwnedZones.zones) do
local zPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and
currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
return closestZone, shortestDist
end
function cfxOwnedZones.getNearestOwnedZone(theZone)
local shortestDist = math.huge
local closestZone = nil
local aPoint = cfxZones.getPoint(theZone)
for zKey, aZone in pairs(cfxOwnedZones.zones) do
local zPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
return closestZone, shortestDist
end
function cfxOwnedZones.getNearestEnemyOwnedZone(theZone, targetNeutral)
if not targetNeutral then targetNeutral = false else targetNeutral = true end
local shortestDist = math.huge
local closestZone = nil
local ourEnemy = dcsCommon.getEnemyCoalitionFor(theZone.owner)
if not ourEnemy then return nil end -- we called for a neutral zone. they have no enemies
local zPoint = cfxZones.getPoint(theZone)
for zKey, aZone in pairs(cfxOwnedZones.zones) do
if targetNeutral then
-- return all zones that do not belong to us
if aZone.owner ~= theZone.owner then
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(aPoint, zPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
else
-- return zones that are taken by the Enenmy
if aZone.owner == ourEnemy then -- only check own zones
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
end
end
return closestZone, shortestDist
end
function cfxOwnedZones.getNearestFriendlyZone(theZone, targetNeutral)
if not targetNeutral then targetNeutral = false else targetNeutral = true end
local shortestDist = math.huge
local closestZone = nil
local ourEnemy = dcsCommon.getEnemyCoalitionFor(theZone.owner)
if not ourEnemy then return nil end -- we called for a neutral zone. they have no enemies nor friends, all zones would be legal.
local zPoint = cfxZones.getPoint(theZone)
for zKey, aZone in pairs(cfxOwnedZones.zones) do
if targetNeutral then
-- target all zones that do not belong to the enemy
if aZone.owner ~= ourEnemy then
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
else
-- only target zones that are taken by us
if aZone.owner == theZone.owner then -- only check own zones
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
end
end
return closestZone, shortestDist
end
function cfxOwnedZones.enemiesRemaining(aZone)
if cfxOwnedZones.getNearestEnemyOwnedZone(aZone) then return true end
return false
end
function cfxOwnedZones.spawnAttackTroops(theTypes, aZone, aCoalition, aFormation)
local unitTypes = {} -- build type names
-- split theTypes into an array of types
unitTypes = dcsCommon.splitString(theTypes, ",")
if #unitTypes < 1 then
table.insert(unitTypes, "Soldier M4") -- make it one m4 trooper as fallback
-- simply exit, no troops specified
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: no attackers for " .. aZone.name .. ". exiting", 30)
end
return
end
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: spawning attackers for " .. aZone.name, 30)
end
--local theCountry = dcsCommon.coalition2county(aCoalition)
local spawnPoint = {x = aZone.point.x, y = aZone.point.y, z = aZone.point.z} -- copy struct
local rads = aZone.attackPhi * 0.01745
spawnPoint.x = spawnPoint.x + math.cos(aZone.attackPhi) * aZone.attackDelta
spawnPoint.y = spawnPoint.y + math.sin(aZone.attackPhi) * aZone.attackDelta
local spawnZone = cfxZones.createSimpleZone("attkSpawnZone", spawnPoint, aZone.attackRadius)
local theGroup = cfxZones.createGroundUnitsInZoneForCoalition (
aCoalition, -- theCountry,
aZone.name .. " (A) " .. dcsCommon.numberUUID(), -- must be unique
spawnZone,
unitTypes,
aFormation, -- outward facing
0)
return theGroup
end
function cfxOwnedZones.spawnDefensiveTroops(theTypes, aZone, aCoalition, aFormation)
local unitTypes = {} -- build type names
-- split theTypes into an array of types
unitTypes = dcsCommon.splitString(theTypes, ",")
if #unitTypes < 1 then
table.insert(unitTypes, "Soldier M4") -- make it one m4 trooper as fallback
-- simply exit, no troops specified
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: no defenders for " .. aZone.name .. ". exiting", 30)
end
return
end
--local theCountry = dcsCommon.coalition2county(aCoalition)
local spawnZone = cfxZones.createSimpleZone("spawnZone", aZone.point, aZone.spawnRadius)
local theGroup = cfxZones.createGroundUnitsInZoneForCoalition (
aCoalition, --theCountry,
aZone.name .. " (D) " .. dcsCommon.numberUUID(), -- must be unique
spawnZone, unitTypes,
aFormation, -- outward facing
0)
return theGroup
end
--
-- U P D A T E
--
function cfxOwnedZones.sendOutAttackers(aZone)
-- only spawn if there are zones to attack
if not cfxOwnedZones.enemiesRemaining(aZone) then
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ - no enemies, resting ".. aZone.name, 30)
end
return
end
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ - attack cycle for ".. aZone.name, 30)
end
-- load the attacker typestring
-- step one: get the attackers
local attackers = aZone.attackersRED;
if (aZone.owner == 2) then attackers = aZone.attackersBLUE end
if attackers == "none" then return end
local theGroup = cfxOwnedZones.spawnAttackTroops(attackers, aZone, aZone.owner, aZone.attackFormation)
-- submit them to ground troops handler as zoneseekers
-- and our groundTroops module will handle the rest
if cfxGroundTroops then
local troops = cfxGroundTroops.createGroundTroops(theGroup)
troops.orders = "attackOwnedZone"
troops.side = aZone.owner
cfxGroundTroops.addGroundTroopsToPool(troops) -- hand off to ground troops
else
if cfxOwnedZones.verbose then
trigger.action.outText("+++ Owned Zones: no ground troops module on send out attackers", 30)
end
end
end
-- bang support
function cfxOwnedZones.bangNeutral(value)
if not cfxOwnedZones.neutralTriggerFlag then return end
local newVal = trigger.misc.getUserFlag(cfxOwnedZones.neutralTriggerFlag) + value
trigger.action.setUserFlag(cfxOwnedZones.neutralTriggerFlag, newVal)
end
function cfxOwnedZones.bangRed(value)
if not cfxOwnedZones.redTriggerFlag then return end
local newVal = trigger.misc.getUserFlag(cfxOwnedZones.redTriggerFlag) + value
trigger.action.setUserFlag(cfxOwnedZones.redTriggerFlag, newVal)
end
function cfxOwnedZones.bangBlue(value)
if not cfxOwnedZones.blueTriggerFlag then return end
local newVal = trigger.misc.getUserFlag(cfxOwnedZones.blueTriggerFlag) + value
trigger.action.setUserFlag(cfxOwnedZones.blueTriggerFlag, newVal)
end
function cfxOwnedZones.bangSide(theSide, value)
if theSide == 2 then
cfxOwnedZones.bangBlue(value)
return
end
if theSide == 1 then
cfxOwnedZones.bangRed(value)
return
end
cfxOwnedZones.bangNeutral(value)
end
function cfxOwnedZones.zoneConquered(aZone, theSide, formerOwner) -- 0 = neutral 1 = RED 2 = BLUE
local who = "REDFORCE"
if theSide == 2 then who = "BLUEFORCE" end
if cfxOwnedZones.announcer then
trigger.action.outText(who .. " have secured zone " .. aZone.name, 30)
aZone.owner = theSide
-- play different sounds depending on who's won
if theSide == 1 then
trigger.action.outSoundForCoalition(1, "Quest Snare 3.wav")
trigger.action.outSoundForCoalition(2, "Death BRASS.wav")
else
trigger.action.outSoundForCoalition(2, "Quest Snare 3.wav")
trigger.action.outSoundForCoalition(1, "Death BRASS.wav")
end
end
-- invoke callbacks now
cfxOwnedZones.invokeConqueredCallbacks(aZone, theSide, formerOwner)
-- bang! flag support
cfxOwnedZones.bangSide(theSide, 1) -- winner
cfxOwnedZones.bangSide(formerOwner, -1) -- loser
-- update map
cfxOwnedZones.drawZoneInMap(aZone) -- update status in map. will erase previous version
-- remove all defenders to avoid shock state
aZone.defenders = nil
-- change to captured
aZone.state = "captured"
aZone.timeStamp = timer.getTime()
end
function cfxOwnedZones.repairDefenders(aZone)
--trigger.action.outText("+++ enter repair for ".. aZone.name, 30)
-- find a unit that is missing from my typestring and replace it
-- one by one until we are back to full strength
-- step one: get the defenders and create a type array
local defenders = aZone.defendersRED;
if (aZone.owner == 2) then defenders = aZone.defendersBLUE end
local unitTypes = {} -- build type names
-- if none, we are done
if defenders == "none" then return end
-- split theTypes into an array of types
allTypes = dcsCommon.trimArray(
dcsCommon.splitString(defenders, ",")
)
local livingTypes = {} -- init to emtpy, so we can add to it if none are alive
if (aZone.defenders) then
-- some remain. add one of the killed
livingTypes = dcsCommon.getGroupTypes(aZone.defenders)
-- we now iterate over the living types, and remove their
-- counterparts from the allTypes. We then take the first that
-- is left
if #livingTypes > 0 then
for key, aType in pairs (livingTypes) do
if not dcsCommon.findAndRemoveFromTable(allTypes, aType) then
trigger.action.outText("+++OwdZ WARNING: found unmatched type <" .. aType .. "> while trying to repair defenders for ".. aZone.name, 30)
else
-- all good
end
end
end
end
-- when we get here, allTypes is reduced to those that have been killed
if #allTypes < 1 then
trigger.action.outText("+++owdZ: WARNING: all types exist when repairing defenders for ".. aZone.name, 30)
else
table.insert(livingTypes, allTypes[1]) -- we simply use the first that we find
end
-- remove the old defenders
if aZone.defenders then
aZone.defenders:destroy()
end
-- now livingTypes holds the full array of units we need to spawn
local theCountry = dcsCommon.getACountryForCoalition(aZone.owner)
local spawnZone = cfxZones.createSimpleZone("spawnZone", aZone.point, aZone.spawnRadius)
local theGroup = cfxZones.createGroundUnitsInZoneForCoalition (
aZone.owner, -- was wrongly: theCountry
aZone.name .. dcsCommon.numberUUID(), -- must be unique
spawnZone,
livingTypes,
aZone.formation, -- outward facing
0)
aZone.defenders = theGroup
aZone.lastDefenders = theGroup:getSize()
end
function cfxOwnedZones.inShock(aZone)
-- a unit was destroyed, everyone else is in shock, no rerpairs
-- group can re-shock when another unit is destroyed
end
function cfxOwnedZones.spawnDefenders(aZone)
local defenders = aZone.defendersRED;
if (aZone.owner == 2) then defenders = aZone.defendersBLUE end
-- before we spawn new defenders, remove the old ones
if aZone.defenders then
if aZone.defenders:isExist() then
aZone.defenders:destroy()
end
aZone.defenders = nil
end
-- if 'none', simply exit
if defenders == "none" then return end
local theGroup = cfxOwnedZones.spawnDefensiveTroops(defenders, aZone, aZone.owner, aZone.formation)
-- the troops reamin, so no orders to move, no handing off to ground troop manager
aZone.defenders = theGroup;
if theGroup then
aZone.defenderMax = theGroup:getInitialSize() -- so we can determine if some units were destroyed
aZone.lastDefenders = aZone.defenderMax -- if this is larger than current number, someone bit the dust
--trigger.action.outText("+++ spawned defenders for ".. aZone.name, 30)
else
trigger.action.outText("+++owdZ: WARNING: spawned no defenders for ".. aZone.name, 30)
end
end
--
-- per-zone update, run down the FSM to determine what to do.
-- FSM uses timeStamp since when state was set. Possible states are
-- - init -- has just been inited for the first time. will usually immediately produce defenders,
-- and then transition to defending
-- - catured -- has just been captured. transition to defending
-- - defending -- wait until timer has reached goal, then produce defending units and transition to attacking.
-- - attacking -- wait until timer has reached goal, and then produce attacking units and send them to closest enemy zone.
-- state is interrupted as soon as a defensive unit is lost. state then goes to defending with timer starting
-- - idle - do nothing, zone's actions are turned off
-- - shocked -- a unit was destroyed. group is in shock for a time until it starts repairing. If another unit is
-- destroyed during the shocked period, the timer resets to zero and repairs are delayed
-- - repairing -- as long as we aren't at full strength, units get replaced one by one until at full strength
-- each time the timer counts down, another missing unit is replaced, and all other unit's health
-- is reset to 100%
--
-- a Zone with the paused attribute set to true will cause it to not do anything
--
-- check if defenders are specified
function cfxOwnedZones.usesDefenders(aZone)
if aZone.owner == 0 then return false end
local defenders = aZone.defendersRED;
if (aZone.owner == 2) then defenders = aZone.defendersBLUE end
return defenders ~= "none"
end
function cfxOwnedZones.usesAttackers(aZone)
if aZone.owner == 0 then return false end
local attackers = aZone.attackersRED;
if (aZone.owner == 2) then defenders = aZone.attackersBLUE end
return attackers ~= "none"
end
function cfxOwnedZones.updateZone(aZone)
-- a zone can be paused, causing it to not progress anything
-- even if zone status is still init, will NOT produce anything
-- if paused is on.
if aZone.paused then return end
nextState = aZone.state;
-- first, check if my defenders have been attacked and one of them has been killed
-- if so, we immediately switch to 'shocked'
if cfxOwnedZones.usesDefenders(aZone) and
aZone.defenders then
-- we have defenders
if aZone.defenders:isExist() then
-- isee if group was damaged
if aZone.defenders:getSize() < aZone.lastDefenders then
-- yes, at least one unit destroyed
aZone.timeStamp = timer.getTime()
aZone.lastDefenders = aZone.defenders:getSize()
if aZone.lastDefenders == 0 then
aZone.defenders = nil
end
aZone.state = "shocked"
return
end
else
-- group was destroyed. erase link, and go into shock for the last time
aZone.state = "shocked"
aZone.timeStamp = timer.getTime()
aZone.lastDefenders = 0
aZone.defenders = nil
return
end
end
if aZone.state == "init" then
-- during init we instantly create the defenders since
-- we assume the zone existed already
if aZone.owner > 0 then
cfxOwnedZones.spawnDefenders(aZone)
-- now drop into attacking mode to produce attackers
nextState = "attacking"
else
nextState = "idle"
end
aZone.timeStamp = timer.getTime()
elseif aZone.state == "idle" then
-- nothing to do, zone is effectively switched off.
-- used for neutal zones or when forced to turn off
-- in some special cases
elseif aZone.state == "captured" then
-- start the clock on defenders
nextState = "defending"
aZone.timeStamp = timer.getTime()
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: State " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name, 30)
end
elseif aZone.state == "defending" then
if timer.getTime() > aZone.timeStamp + cfxOwnedZones.defendingTime then
cfxOwnedZones.spawnDefenders(aZone)
-- now drop into attacking mode to produce attackers
nextState = "attacking"
aZone.timeStamp = timer.getTime()
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: State " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name, 30)
end
end
elseif aZone.state == "repairing" then
-- we are currently rebuilding defenders unit by unit
if timer.getTime() > aZone.timeStamp + cfxOwnedZones.repairTime then
aZone.timeStamp = timer.getTime()
cfxOwnedZones.repairDefenders(aZone)
if aZone.defenders:getSize() >= aZone.defenderMax then
--
-- we are at max size, time to produce some attackers
nextState = "attacking"
aZone.timeStamp = timer.getTime()
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: State " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name, 30)
end
end
-- see if we are full strenght and if so go to attack, else set timer to reair the next unit
end
elseif aZone.state == "shocked" then
-- we are currently rebuilding defenders unit by unit
if timer.getTime() > aZone.timeStamp + cfxOwnedZones.shockTime then
nextState = "repairing"
aZone.timeStamp = timer.getTime()
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: State " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name, 30)
end
end
elseif aZone.state == "attacking" then
if timer.getTime() > aZone.timeStamp + cfxOwnedZones.attackingTime then
cfxOwnedZones.sendOutAttackers(aZone)
-- reset timer
aZone.timeStamp = timer.getTime()
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: State " .. aZone.state .. " reset for " .. aZone.name, 30)
end
end
else
-- unknown zone state
end
aZone.state = nextState
end
function cfxOwnedZones.update()
cfxOwnedZones.updateSchedule = timer.scheduleFunction(cfxOwnedZones.update, {}, timer.getTime() + 1/cfxOwnedZones.ups)
-- iterate all zones, and determine their current ownership status
for key, aZone in pairs(cfxOwnedZones.zones) do
-- a hand change can only happen if there are only ground troops from the OTHER side in
-- the zone
local categ = Group.Category.GROUND
local theBlues = cfxZones.groupsOfCoalitionPartiallyInZone(2, aZone, categ)
local theReds = cfxZones.groupsOfCoalitionPartiallyInZone(1, aZone, categ)
local currentOwner = aZone.owner
if #theBlues > 0 and #theReds == 0 and aZone.unbeatable ~= true then
-- this now belongs to blue
if currentOwner ~= 2 then
cfxOwnedZones.zoneConquered(aZone, 2, currentOwner)
end
elseif #theBlues == 0 and #theReds > 0 and aZone.unbeatable ~= true then
-- this now belongs to red
if currentOwner ~= 1 then
cfxOwnedZones.zoneConquered(aZone, 1, currentOwner)
end
end
-- now, perhaps with their new owner call updateZone()
cfxOwnedZones.updateZone(aZone)
end
end
function cfxOwnedZones.sideOwnsAll(theSide)
for key, aZone in pairs(cfxOwnedZones.zones) do
if aZone.owner ~= theSide then
return false
end
end
-- if we get here, all your base are belong to us
return true
end
function cfxOwnedZones.hasOwnedZones()
for idx, zone in pairs (cfxOwnedZones.zones) do
return true -- even the first returns true
end
-- no owned zones
return false
end
function cfxOwnedZones.readConfigZone(theZone)
cfxOwnedZones.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
cfxOwnedZones.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true)
if cfxZones.hasProperty(theZone, "r!") then
cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "r!", "<none>")
end
if cfxZones.hasProperty(theZone, "b!") then
cfxOwnedZones.blueTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "b!", "<none>")
end
if cfxZones.hasProperty(theZone, "n!") then
cfxOwnedZones.neutralTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "n!", "<none>")
end
cfxOwnedZones.defendingTime = cfxZones.getNumberFromZoneProperty(theZone, "defendingTime", 100)
cfxOwnedZones.attackingTime = cfxZones.getNumberFromZoneProperty(theZone, "attackingTime", 300)
cfxOwnedZones.shockTime = cfxZones.getNumberFromZoneProperty(theZone, "shockTime", 200)
cfxOwnedZones.repairTime = cfxZones.getNumberFromZoneProperty(theZone, "repairTime", 200)
end
function cfxOwnedZones.init()
-- check libs
if not dcsCommon.libCheck("cfx Owned Zones",
cfxOwnedZones.requiredLibs) then
return false
end
-- read my config zone
local theZone = cfxZones.getZoneByName("ownedZonesConfig")
if not theZone then
trigger.action.outText("+++ownZ: no config", 30)
else
cfxOwnedZones.readConfigZone(theZone)
end
-- collect all owned zones by their 'owner' property
-- start the process
local pZones = cfxZones.zonesWithProperty("owner")
-- now add all zones to my zones table, and convert the owner property into
-- a proper attribute
for k, aZone in pairs(pZones) do
cfxOwnedZones.addOwnedZone(aZone)
end
initialized = true
cfxOwnedZones.updateSchedule = timer.scheduleFunction(cfxOwnedZones.update, {}, timer.getTime() + 1/cfxOwnedZones.ups)
trigger.action.outText("cx/x owned zones v".. cfxOwnedZones.version .. " started", 30)
return true
end
if not cfxOwnedZones.init() then
trigger.action.outText("cf/x Owned Zones aborted: missing libraries", 30)
cfxOwnedZones = nil
end

644
modules/cfxPlayer.lua Normal file
View File

@ -0,0 +1,644 @@
-- cfx player handler for DCS Missions by cf/x AG
--
-- a module that provides easy access to a mission's player data
-- multi-player only
--
cfxPlayer = {}
-- a call to cfxPlayer.start()
cfxPlayer.version = "3.0.1"
--[[-- VERSION HISTORY
- 2.2.3 - fixed isPlayerUnit() wrong return of true instead of nil
- 2.2.4 - getFirstGroupPlayer
- 2.3.0 - added event filtering for monitors
- limited code clean-up
- removed XXXmatchUnitToPlayer
- corrected isPlayerUnit once more
- removed autostart option
- removed detectPlayersLeaving option
- 3.0.0 - added detection of network players
- added new events newPlayer, changePlayer
- and leavePlayer (never called)
- 3.0.1 - isPlayerUnit guard against scenery object or map object
--]]--
cfxPlayer.verbose = false;
cfxPlayer.running = false
cfxPlayer.ups = 1 -- updates per second: how often do we query the players
-- a good value is 1
cfxPlayer.playerDB = {} -- the list of all player UNITS
-- attributes
-- name - name of unit occupied by player
-- unit - unit this player is controlling
-- unitName - same as name
-- group - group this unit belongs to. can't change without also changing unit
-- groupName - name of group
-- coalition
cfxPlayerGroups = {} -- GLOBAL VAR
-- list of all current groups that have players in them
-- can call out to handlers if group is added or removed
-- use this in MP games to organise messaging and keep score
-- by default, groupinfo merely contains the .group reference
-- and is accessed by name as key which is also accessible by .name
cfxPlayer.netPlayers = {} -- new for version 3: real player detection
-- a dict sorted by player name that containts the unit name for last pass
cfxPlayer.updateSchedule = 0 -- ID used for scheduling update
cfxPlayer.coalitionSides = {0, 1, 2} -- we currently have neutral, red, blue
cfxPlayer.monitors = {} -- callbacks for events
---
-- structure of playerInfo
-- - name - player's unit name
-- - unit - the unit the player is occupying. Multi-Crew: many people can be in same unit
-- - unitName same as name
-- - coalition - the side the unit is on, as a number
function cfxPlayer.dumpRawPlayers()
trigger.action.outText("+++ debug: raw player dump ---", 30)
for i=1, #cfxPlayer.coalitionSides do
local theSide = cfxPlayer.coalitionSides[i]
-- get all players for this side
local thePlayers = coalition.getPlayers(theSide)
for p=1, #thePlayers do
aPlayerUnit = thePlayers[p] -- docs say this is a unit table, not a person table!
trigger.action.outText(i .. "-" .. p ..": unit: " .. aPlayerUnit:getName() .. " controlled by " .. aPlayerUnit:getPlayerName() , 30)
end
end
trigger.action.outText("+++ debug: END DUMP ----", 30)
end
function cfxPlayer.getAllPlayers()
return cfxPlayer.playerDB -- get entire db. make sure not to screw around with it
end
function cfxPlayer.getPlayerInfoByName(theUnitName) -- note: UNIT name
thePlayer = cfxPlayer.playerDB[theUnitName] -- access the entry, not we are accessing by unit name
return thePlayer
end
function cfxPlayer.getPlayerInfoByIndex(theIndex)
local enumeratedInfo = dcsCommon.enumerateTable(cfxPlayer.playerDB)
if (theIndex > #enumeratedInfo) then
trigger.action.outText("WARNING: player index " .. theIndex .. " out of bounds - max = " .. #enumeratedInfo, 30)
return nil end
if (theIndex < 1) then return nil end
return enumeratedInfo[theIndex]
end
-- this is now a true/false function that returns true if unit is player
function cfxPlayer.XXXmatchUnitToPlayer(theUnit) -- what's difference to getPlayerInfo? GetPlayerInfo ALLOCATES if not exists
if not (theUnit) then return false end
if not (theUnit:isExist()) then return false end
-- PATCH: if theUnit:getPlayerName() returns anything but nil
-- this is a player unit
-- unfortunately, this can sometimes fail
-- so make sure the function existst
-- it failed because the next level up function
-- returned true if i returned anything but nil, and I return
-- true or false, both not nil
-- this proc works
if not theUnit.getPlayerName then return false end
local pName = theUnit:getPlayerName()
if pName ~= nil then
-- trigger.action.outText("+++matchUnit: player name " .. pName .. " for unit " .. theUnit:getName(), 30)
return true
end
if (true) then
return false
end
-- ignmore old code below
for pname, pInfo in pairs(cfxPlayer.playerDB) do
if (pInfo.unit == theUnit) then
return pInfo
end
end
return nil
end
function cfxPlayer.XXXisPlayerUnitAlt(theUnit)
for pname, pInfo in pairs(cfxPlayer.playerDB) do
if (pInfo.unit == theUnit) then
return true
end
end
return false
end
function cfxPlayer.isPlayerUnit(theUnit)
-- new patch. simply check if getPlayerName returns something
if not theUnit then return false end
if not theUnit.getPlayerName then return false end -- map/static object
local pName = theUnit:getPlayerName()
if pName then return true end
return false
--
-- fixed, erroneously expected a nil from matchUnitToPlayer
--return (cfxPlayer.matchUnitToPlayer(theUnit)) -- was: ~=nil, wrong because match returns true/false
end
function cfxPlayer.getPlayerUnitType(thePlayerInfo) --
if (thePlayerInfo) then
theUnit = thePlayerInfo.unit
if (theUnit) and (theUnit:isExist()) then
return theUnit:getTypeName()
end
end
return nil
end
-- get player's unit info
-- accesses player DB and returns the player's info record for the
-- player's Unit. If record does not exist in db, a new record is allocated
-- returns true if verification succeeeds: player unit existed before, and
-- false otherwise. in the latter case, A NEW playerInfo object is returned
function cfxPlayer.getPlayerInfo(theUnit)
local playerName = theUnit:getPlayerName() -- retrieve the name
--- PATCH!!!!!!!
--- on multi-crew, we only have the pilot as getPlayerName.
--- we now switch to the unit's name instead
playerName = theUnit:getName()
-- trigger.action.outText("Player: ".. playerName, 10)
local existingPlayer = cfxPlayer.getPlayerInfoByName(playerName) -- try and access DB
if existingPlayer then
-- this player exists in the db. return the record
return true, existingPlayer;
else
-- this is a new player.
-- set up a new playerinfo record for this name
local newPlayerInfo = {}
newPlayerInfo.name = playerName
newPlayerInfo.unit = theUnit
newPlayerInfo.unitName = theUnit:getName()
newPlayerInfo.group = theUnit:getGroup()
newPlayerInfo.groupName = newPlayerInfo.group:getName()
newPlayerInfo.coalition = theUnit:getCoalition() -- seems to work when first param is class self
-- note that this record did not exist, and return record
return false, newPlayerInfo
end
end;
function cfxPlayer.getSinglePlayerAirframe()
-- ALWAYS return a string! This is for debugging purposes
local thePlayers = {}
local count = 0
local theAirframe = "(none)"
for pname, pinfo in pairs(cfxPlayer.playerDB) do
count = count + 1
theAirframe = pinfo.unit:getTypeName()
end
if count < 2 then return theAirframe end -- also returns if count == 0
return "<Multiplayer Not Yet Supported>"
end
function cfxPlayer.getAnyPlayerAirframe()
-- use this for debugging, in single-player missions, or where it
-- is unimportant which player, just a player
-- assumes that all players use the same airframe or are in the
-- same group / unit
for pname, pinfo in pairs(cfxPlayer.playerDB) do
if (pinfo.unit:isExist()) then
-- player may just have crashed or left
local theAirframe = pinfo.unit:getTypeName()
return theAirframe -- we simply stop after first successfuly access
end
end
return "error: no player"
end
function cfxPlayer.getFirstGroupPlayerName(theGroup)
-- get the name of player of the first
-- player-controlled unit I come across in
-- this group
local allGroupUnits = theGroup:getUnits()
for ukey, uvalue in pairs(allGroupUnits) do
-- iterate units in group
if (uvalue:isExist()) then -- and cfxPlayer.isPlayerUnit(uvalue))
-- player may just have crashed or left
-- but all units in the same group have the same type when they are aircraft
if cfxPlayer.isPlayerUnit(uvalue) then
return uvalue:getPlayerName(), uvalue
end
end
end
return nil
end
function cfxPlayer.getAnyGroupPlayerAirframe(theGroup)
-- get the first player-driven unit in the group
-- and pass back the airframe that is being used
local allGroupUnits = theGroup:getUnits()
for ukey, uvalue in pairs(allGroupUnits) do
if (uvalue:isExist()) then -- and cfxPlayer.isPlayerUnit(uvalue))
-- player may just have crashed or left
-- but all units in the same group have the same type when they are aircraft
local theAirframe = uvalue:getTypeName()
return theAirframe -- we simply stop after first successfuly access
end
end
return "error: no live player in group "
end
function cfxPlayer.getAnyPlayerPosition()
-- use this for debugging, in single-player missions, or where it
-- is unimportant which player, just a player
-- will cause issues when you derive location info or group info
-- from that player
for pname, pinfo in pairs(cfxPlayer.playerDB) do
if (pinfo.unit:isExist()) then
local thePoint = pinfo.unit:getPoint()
return thePoint -- we simply stop after first successfuly access
end
end
return nil
end
function cfxPlayer.getAnyGroupPlayerPosition(theGroup)
-- enter with dcs group to search for player units within
-- step one: get all units that belong to that group
local allGroupUnits = theGroup:getUnits()
-- we now iterate all returned units and look for
-- a unit that is a player unit.
for ukey, uvalue in pairs(allGroupUnits) do
-- we currently assume single-unit groups for players
if (uvalue:isExist()) then -- and cfxPlayer.isPlayerUnit(uvalue))
-- player may just have crashed or left
local thePoint = uvalue:getPoint()
return thePoint -- we simply stop after first successfuly access
end
end
return nil
end
function cfxPlayer.getAnyGroupPlayerInfo(theGroup)
for pname, pinfo in pairs(cfxPlayer.playerDB) do
if (pinfo.unit:isExist() and pinfo.group == theGroup) then
return pinfo -- we simply stop after first successfuly access
end
end
return "error: no player"
end
function cfxPlayer.getAllPlayerGroups()
-- merely accessot. better would be returning a copy
return cfxPlayerGroups
end
function cfxPlayer.getGroupDataForGroupNamed(name)
if not name then return nil end
return cfxPlayerGroups[name]
end
function cfxPlayer.getPlayersInGroup(theGroup)
if not theGroup then return {} end
if not theGroup:isExist() then return {} end
local gName = theGroup:getName()
local thePlayers = {}
for pname, pinfo in pairs(cfxPlayer.playerDB) do
local pgName = ""
if pinfo.group:isExist() then pgName = pinfo.group:getName() end
if (gName == pgName) then
table.insert(thePlayers, pinfo)
end
end
return thePlayers
end
-- update() is called regularly to check up on the players
-- when a mismatch to last player state is found, callbacks
-- can be invoked
function cfxPlayer.update()
-- first, re-schedule my next invocation
cfxPlayer.updateSchedule = timer.scheduleFunction(cfxPlayer.update, {}, timer.getTime() + 1/cfxPlayer.ups)
-- now scan the coalitions for all players
local currCount = 0 -- number of players found this pass
local currDB = {} -- db of player units this pass
local currPlayerUnitsByNames = {}
-- iterate over all colaitions
for i=1, #cfxPlayer.coalitionSides do
local theSide = cfxPlayer.coalitionSides[i]
-- get all player units for this side
local thePlayers = coalition.getPlayers(theSide) -- returns UNITs!!!
for p=1, #thePlayers do
-- we now iterate the Units and compare what we find
local thePlayerUnit = thePlayers[p]
local isExistingPlayerUnit, theInfo = cfxPlayer.getPlayerInfo(thePlayerUnit)
if (not isExistingPlayerUnit) then
-- add Unit (not player!) to db
cfxPlayer.playerDB [theInfo.name] = theInfo
cfxPlayer.invokeMonitorsForEvent("new", "Player Unit " .. theInfo.name .. " entered mission", theInfo, {})
else
-- player's unit existed last time around
-- see if something changed:
-- currently, we track units, not players. side changes for units can't happen AT ALL
if theInfo.coalition ~= thePlayerUnit:getCoalition() then
local theData = {}
theData.old = theInfo.coalition
theData.new = thePlayerUnit:getCoalition()
-- we invoke a callback
cfxPlayer.invokeMonitorsForEvent("side", "Player " .. theInfo.name .. " switched sides to " .. thePlayerUnit:getCoalition(), theInfo, theData)
end;
-- we now check if the player has changed groups
-- sinced we track units, this CANT HAPPEN AT ALL
if theInfo.group ~= thePlayerUnit:getGroup() then
local theData = {}
theData.old = theInfo.group
theData.new = thePlayerUnit:getGroup()
cfxPlayer.invokeMonitorsForEvent("group", "Player changed group to " .. thePlayerUnit:getGroup():getName(), theInfo, theData)
trigger.action.outText("+++ debug: Player " .. theInfo.name .. " changed GROUP to: " .. thePlayerUnit:getGroup():getName(), 30)
end
-- we should now check if the player has changed units
-- since we track units, this cant happen at all
if theInfo.unit ~= thePlayerUnit then
-- player changed unit
local theData = {}
theData.old = theInfo.unit
-- the old unit's name is still available in theInfo.unitName
theData.oldUnitName = theInfo.unitName
theData.new = thePlayerUnit
-- update Player Info
cfxPlayer.invokeMonitorsForEvent("unit", "Player changed unit to " .. thePlayerUnit:getName(), theInfo, theData)
end
-- update the playerEntry. always done
theInfo.unit = thePlayerUnit
theInfo.unitName = thePlayerUnit:getName()
theInfo.coalition = thePlayerUnit:getCoalition()
theInfo.group = thePlayerUnit:getGroup()
end;
-- add this entry to current pass db so we can detect
-- any discrepancies to last pass
currDB[theInfo.name] = theInfo
-- now update current network player name db
local playerUnitName = thePlayerUnit:getName()
if not thePlayerUnit:isExist() then playerUnitName = "<none>" end
currPlayerUnitsByNames[thePlayerUnit:getPlayerName()] = playerUnitName
end -- for all player units of this side
end -- for all sides
-- we can now check if a player unit has disappeared
-- we do this by checking that all old entries from cfxPlayer.playerDB
-- have an existing counterpart in new currDB
for name, info in pairs(cfxPlayer.playerDB) do
local matchingEntry = currDB[name]
if matchingEntry then
-- allright nothing to do
else
-- whoa, this record is missing!
-- do we care?
if true then -- (cfxPlayer.detectPlayersLeaving) then
-- yes! trigger an event
cfxPlayer.invokeMonitorsForEvent("leave", "Player left mission", info, {})
-- we don't need to destroy entry, as we simply replace the
-- playerDB with currDB at end of update
else
-- no, just copy old data over. They'll be back
currDB[name] = info
end
end
end;
-- we now perform a group check and update all groups for players
local currPlayerGroups = {}
for pName, pInfo in pairs(currDB) do
-- retrieve player unit and make sure it still exists
local theUnit = pInfo.unit
if theUnit:isExist() then
-- yeah, it exists allright. let's get to the group
local theGroup = theUnit:getGroup()
local gName = theGroup:getName()
-- see if this group is new
local thePGroup = cfxPlayerGroups[gName]
if not thePGroup then
-- allocate new group
thePGroup = {}
thePGroup.group = theGroup
thePGroup.name = gName
thePGroup.primeUnit = theUnit -- may be used as fallback
thePGroup.primeUnitName = theUnit:getName() -- also fallback only
thePGroup.id = theGroup:getID()
cfxPlayer.invokeMonitorsForEvent("newGroup", "New Player Group " .. gName .. " appeared", nil, thePGroup)
end
currPlayerGroups[gName] = thePGroup -- update group table
end
end
-- now check if a player group has disappeared
for gkey, gval in pairs(cfxPlayerGroups) do
if not currPlayerGroups[gkey] then
cfxPlayer.invokeMonitorsForEvent("removeGroup", "A Player Group " .. gkey .. " vanished", nil, gval) -- gval is OLD set, contains group
end
end
-- version 3 addion: track network players
-- see if a new player has appeared
for aPlayerName, aPlayerUnitName in pairs(currPlayerUnitsByNames) do
-- see if this name was already in last
if cfxPlayer.netPlayers[aPlayerName] then
-- yes. but was it the same unit?
if cfxPlayer.netPlayers[aPlayerName] == currPlayerUnitsByNames[aPlayerName] then
-- all is well, no change
else
-- player has changed units
-- since they can't disappear,
-- this event can happen
local data = {}
data.oldUnitName = cfxPlayer.netPlayers[aPlayerName]
data.newUnitName = aPlayerUnitName
data.playerName = aPlayerName
if aPlayerUnitName == "" then aPlayerUnitName = "<none>" end
if aPlayerUnitName == "<none>" then
-- unit no longer exists, player probably dead,
-- parachuting or spectating. Maybe even left game
-- resgisters as 'change' -- is 'left unit'
cfxPlayer.invokeMonitorsForEvent("changePlayer", "A Player left unit " .. data.oldUnitName, nil, data)
else
-- changed to new unit
cfxPlayer.invokeMonitorsForEvent("changePlayer", "A Player changed to unit " .. aPlayerUnitName, nil, data)
end
end
else
-- this is a new player
local data = {}
data.playerName = aPlayerName
data.newUnitName = aPlayerUnitName
cfxPlayer.invokeMonitorsForEvent("newPlayer", "New Player appeared " .. aPlayerName .. " in unit " .. aPlayerUnitName, nil, data)
end
end
-- version 3: detect if a player left
for oldPlayerName, oldUnitName in pairs(cfxPlayer.netPlayers) do
if not currPlayerUnitsByNames[oldPlayerName] then
--local data = {}
--data.playerName = oldPlayerName
--data.oldUnitName = oldUnitName
--cfxPlayer.invokeMonitorsForEvent("leavePlayer", "Player " .. oldPlayerName .. " disappeared from unit " .. oldUnitName, nil, data)
--
-- we keep the player in the db by copying
-- it over and set the unit name to ""
-- will cause at least once 'change' event later
-- probably two in MP
currPlayerUnitsByNames[oldPlayerName] = "<none>"
end
end
-- update playerGroups for this cycle
cfxPlayerGroups = currPlayerGroups
-- update network player for this c<cle
cfxPlayer.netPlayers = currPlayerUnitsByNames
-- finally, we simply replace the old db with the new one
cfxPlayer.playerDB = currDB;
end
function cfxPlayer.getAllNetPlayerNames ()
local themAll = {}
for aName, aUnitName in cfxPlayer.netPlayers do
table.insert(themAll, aName)
end
return themAll
end
function cfxPlayer.getPlayerUnitName(aPlayerName)
if not aPlayerName then return nil end
return cfxPlayer.netPlayers[aPlayerName]
end
function cfxPlayer.isPlayerSeated(aPlayerName)
local unitName = cfxPlayer.getPlayerUnitName(aPlayerName)
if not unitName then return false end
if unitName == "" or unitName == "<none>" then return false end
return true
end
-- add a monitor to be notified of player events
-- may provide a whitelist of events as array of strings
function cfxPlayer.addMonitor(callback, events)
local newMonitor = {}
newMonitor.callback = callback
newMonitor.events = events
cfxPlayer.monitors[callback] = newMonitor
end;
function cfxPlayer.removeMonitor(callback)
if (cfxMonitos[callback]) then
cfxMonitos[callback] = nil
end
end
function cfxPlayer.invokeMonitorsForEvent(evType, description, player, data)
for callback, monitor in pairs(cfxPlayer.monitors) do
-- should filter if evType is in monitor.events
if monitor.events and #monitor.events > 0 then
-- only invoke if this event is listed
if dcsCommon.arrayContainsString(monitor.events, evType) then
monitor.callback(evType, description, player, data)
end
else
monitor.callback(evType, description, player, data)
end
end
end
function cfxPlayer.getAllExistingPlayerUnitsRaw()
local apu = {}
for i=1, #cfxPlayer.coalitionSides do
local theSide = cfxPlayer.coalitionSides[i]
-- get all players for this side
local thePlayers = coalition.getPlayers(theSide)
for p=1, #thePlayers do
local aUnit = thePlayers[p]
if aUnit and aUnit:isExist() then
table.insert(apu, aUnit)
end
end
end
return apu
end
-- evType that can actually happen are 'new', 'leave' for units,
-- 'newGroup' and 'removeGroup' for groups
function cfxPlayer.defaultMonitor(evType, description, info, data)
if cfxPlayer.verbose then
trigger.action.outText("+++Plr - evt '".. evType .."': <" .. description .. ">", 30)
if (info) then
trigger.action.outText("+++Plr: for unit named: " .. info.name, 30)
else
--trigger.action.outText("+++Plr: no player data", 30)
end
--trigger.action.outText("+++Plr: desc: '".. evType .."'<" .. description .. ">", 30)
-- we ignore the data block
end
end
function cfxPlayer.start()
trigger.action.outText("cf/x player v".. cfxPlayer.version .. ": started", 10)
cfxPlayer.running = true
cfxPlayer.update()
end
function cfxPlayer.stop()
if cfxPlayer.verbose then
trigger.action.outText("cf/x player v".. cfxPlayer.version .. ": stopped", 10)
end
timer.removeFunction(cfxPlayer.updateSchedule) -- will require another start() to resume
cfxPlayer.running = false
end
function cfxPlayer.init()
trigger.action.outText("cf/x player v".. cfxPlayer.version .. ": loaded", 10)
-- when verbose, we also add a monitor to display player event
if cfxPlayer.verbose then
cfxPlayer.addMonitor(cfxPlayer.defaultMonitor, {})
trigger.action.outText("cf/x player isd verbose", 10)
end
cfxPlayer.start()
end
-- get everything rolling, but will only start if autostart is true
cfxPlayer.init()
--TODO: player status: ground, air, dead, none
-- TODO: event when status changes ground/air/...

350
modules/cfxPlayerScore.lua Normal file
View File

@ -0,0 +1,350 @@
cfxPlayerScore = {}
cfxPlayerScore.version = "1.2.0"
cfxPlayerScore.badSound = "Death BRASS.wav"
cfxPlayerScore.scoreSound = "Quest Snare 3.wav"
cfxPlayerScore.announcer = true
--[[-- VERSION HISTORY
1.0.1 - bug fixes to killDetected
1.0.2 - messaging clean-up, less verbose
1.1.0 - integrated score base system
- accepts configZones
- module validation
- isNamedUnit(theUnit)
- notify if named unit killed
- kill weapon reported
1.2.0 - score table
- announcer attribute
- badSound name
- scoreSound name
--]]--
cfxPlayerScore.requiredLibs = {
"dcsCommon", -- this is doing score keeping
"cfxPlayer", -- player events, comms
"cfxZones", -- zones for config
}
cfxPlayerScore.playerScore = {} -- init to empty
-- typeScore: dictionary sorted by typeString for score
-- extend to add more types. It is used by unitType2score to
-- determine the base unit score
cfxPlayerScore.typeScore = {}
--
-- we subscribe to the kill event. each time a unit
-- is killed, we check if it was killed by a player
-- and if so, that player record is updated and the side
-- whom the player belongs to is informed
--
cfxPlayerScore.aircraft = 50
cfxPlayerScore.helo = 40
cfxPlayer.ground = 10
cfxPlayerScore.ship = 80
cfxPlayerScore.train = 5
function cfxPlayerScore.cat2BaseScore(inCat)
if inCat == 0 then return cfxPlayerScore.aircraft end -- airplane
if inCat == 1 then return cfxPlayerScore.helo end -- helo
if inCat == 2 then return cfxPlayer.ground end -- ground
if inCat == 3 then return cfxPlayerScore.ship end -- ship
if inCat == 4 then return cfxPlayerScore.train end -- train
trigger.action.outText("+++scr c2bs: unknown category for lookup: <" .. inCat .. ">, returning 1", 30)
return 1
end
function cfxPlayerScore.unit2score(inUnit)
local vicGroup = inUnit:getGroup()
local vicCat = vicGroup:getCategory()
local vicType = inUnit:getTypeName()
local vicName = inUnit:getName()
-- simply extend by adding items to the typescore table.concat
-- we first try by unit name. This allows individual
-- named hi-value targets to have individual scores
local uScore = cfxPlayerScore.typeScore[vicName]
if uScore == nil then
-- WE NOW TRY TO ACCESS BY VICTIM'S TYPE STRING
uScore = cfxPlayerScore.typeScore[vicType]
else
end
if type(uScore) == "string" then
-- convert string to number
uScore = tonumber(uScore)
end
if uScore == nil then uScore = 0 end
if uScore > 0 then return uScore end
-- only apply base scores when the lookup did not give a result
uScore = cfxPlayerScore.cat2BaseScore(vicCat)
return uScore
end
function cfxPlayerScore.getPlayerScore(playerName)
local thePlayerScore = cfxPlayerScore.playerScore[playerName]
if thePlayerScore == nil then
thePlayerScore = {}
thePlayerScore.name = playerName
thePlayerScore.score = 0 -- score
thePlayerScore.killTypes = {} -- the type strings killed
thePlayerScore.totalKills = 0 -- number of kills total
end
return thePlayerScore
end
function cfxPlayerScore.setPlayerScore(playerName, thePlayerScore)
cfxPlayerScore.playerScore[playerName] = thePlayerScore
end
function cfxPlayerScore.updateScoreForPlayer(playerName, score)
local thePlayerScore = cfxPlayerScore.getPlayerScore(playerName)
thePlayerScore.score = thePlayerScore.score + score
cfxPlayerScore.setPlayerScore(playerName, thePlayerScore)
return thePlayerScore.score
end
function cfxPlayerScore.logKillForPlayer(playerName, theUnit)
if not theUnit then return end
if not playerName then return end
local thePlayerScore = cfxPlayerScore.getPlayerScore(playerName)
local theType = theUnit:getTypeName()
local killCount = thePlayerScore.killTypes[theType]
if killCount == nil then
killCount = 0
end
killCount = killCount + 1
thePlayerScore.totalKills = thePlayerScore.totalKills + 1
thePlayerScore.killTypes[theType] = killCount
cfxPlayerScore.setPlayerScore(playerName, thePlayerScore)
end
function cfxPlayerScore.playerScore2text(thePlayerScore)
local desc = thePlayerScore.name .. " - score: ".. thePlayerScore.score .. " - kills: " .. thePlayerScore.totalKills .. "\n"
-- now go through all killSide
for theType, quantity in pairs(thePlayerScore.killTypes) do
desc = desc .. " - " .. theType .. ": " .. quantity .. "\n"
end
return desc
end
function cfxPlayerScore.scoreTextForPlayerNamed(playerName)
local thePlayerScore = cfxPlayerScore.getPlayerScore(playerName)
return cfxPlayerScore.playerScore2text(thePlayerScore)
end
function cfxPlayerScore.isNamedUnit(theUnit)
if not theUnit then return false end
local theName = "(cfx_none)"
if type(theUnit) == "string" then
theName = theUnit -- direct name assignment
-- WARNING: NO EXIST CHECK DONE!
else
-- after kill, unit is dead, so will no longer exist!
theName = theUnit:getName()
if not theName then return false end
end
if cfxPlayerScore.typeScore[theName] then
return true
end
return false
end
--
-- EVENT HANDLING
--
function cfxPlayerScore.preProcessor(theEvent)
-- return true if the event should be processed
-- by us
if theEvent.id == 28 then
-- we only are interested in kill events where
-- there is an initiator, and the initiator is
-- a player
if theEvent.initiator == nil then
-- trigger.action.outText("+++scr pre: nil INITIATOR", 30)
return false
end
--trigger.action.outText("+++scr pre: initiator is " .. theEvent.initiator:getName(), 30)
local killer = theEvent.initiator
if theEvent.target == nil then
if cfxPlayerScore.verbose then trigger.action.outText("+++scr pre: nil TARGET", 30) end
return false
end
local wasPlayer = cfxPlayer.isPlayerUnit(killer)
if wasPlayer then
else
end
return wasPlayer
end
return false
end
function cfxPlayerScore.postProcessor(theEvent)
-- don't do anything
end
function cfxPlayerScore.killDetected(theEvent)
-- we are only getting called when and if
-- a kill occured and killer was a player
-- and target exists
local killer = theEvent.initiator
local killerName = killer:getPlayerName()
if not killerName then killerName = "<nil>" end
local killSide = killer:getCoalition()
local killVehicle = killer:getTypeName()
if not killVehicle then killVehicle = "<nil>" end
local victim = theEvent.target
-- was it a player kill?
local pk = cfxPlayer.isPlayerUnit(victim)
-- was it a scenery object
local wasBuilding = dcsCommon.isSceneryObject(victim)
if wasBuilding then
return
end
-- was it fraternicide?
local vicSide = victim:getCoalition()
local fraternicide = killSide == vicSide
local vicDesc = victim:getTypeName()
local scoreMod = 1 -- start at one
-- see what kind of unit (category) we killed
-- and look up base score
if not victim.getGroup then
if cfxPlayerScore.verbose then trigger.action.outText("+++scr: no group for " .. killVehicle .. ", killed by " .. killerName .. ", no score", 30) end
return
end
local vicGroup = victim:getGroup()
local vicCat = vicGroup:getCategory()
local unitScore = cfxPlayerScore.unit2score(victim)
-- see which weapon was used. gun kills score 2x
local killMeth = ""
local killWeap = theEvent.weapon
if killWeap then
local killWeapType = killWeap:getCategory()
if killWeapType == 0 then
killMeth = " with GUNS"
scoreMod = scoreMod * 2
else
local kWeapon = killWeap:getTypeName()
killMeth = " with " .. kWeapon
end
else
end
if pk then
vicDesc = victim:getPlayerName() .. " in " .. vicDesc
scoreMod = scoreMod * 10
end
if fraternicide then
scoreMod = scoreMod * -2
if cfxPlayerScore.announcer then
trigger.action.outTextForCoalition(killSide, killerName .. " in " .. killVehicle .. " killed FRIENDLY " .. vicDesc .. killMeth .. "!", 30)
trigger.action.outSoundForCoalition(killSide, cfxPlayerScore.badSound)
end
else
if cfxPlayerScore.announcer then
trigger.action.outText(killerName .. " in " .. killVehicle .." killed " .. vicDesc .. killMeth .."!", 30)
trigger.action.outSoundForCoalition(vicSide, cfxPlayerScore.badSound)
trigger.action.outSoundForCoalition(killSide, cfxPlayerScore.scoreSound)
end
-- since not fraticide, log this kill
-- logging kills does not impct score
cfxPlayerScore.logKillForPlayer(killerName, victim)
end
-- see if it was a named target
if cfxPlayerScore.isNamedUnit(victim) then
if cfxPlayerScore.announcer then
trigger.action.outTextForCoalition(killSide, killerName .. " reports killing strategic unit '" .. victim:getName() .. "'", 30)
end
end
local totalScore = unitScore * scoreMod
local playerScore = cfxPlayerScore.updateScoreForPlayer(killerName, totalScore)
if cfxPlayerScore.announcer then
trigger.action.outTextForCoalition(killSide, "Killscore: " .. totalScore .. " for a total of " .. playerScore .. " for " .. killerName, 30)
end
--trigger.action.outTextForCoalition(killSide, cfxPlayerScore.scoreTextForPlayerNamed(killerName), 30)
end
function cfxPlayerScore.readConfigZone(theZone)
cfxPlayerScore.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
-- default scores
cfxPlayerScore.aircraft = cfxZones.getNumberFromZoneProperty(theZone, "aircraft", 50)
cfxPlayerScore.helo = cfxZones.getNumberFromZoneProperty(theZone, "helo", 40)
cfxPlayer.ground = cfxZones.getNumberFromZoneProperty(theZone, "ground", 10)
cfxPlayerScore.ship = cfxZones.getNumberFromZoneProperty(theZone, "ship", 80)
cfxPlayerScore.train = cfxZones.getNumberFromZoneProperty(theZone, "train", 5)
cfxPlayerScore.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true)
if cfxZones.hasProperty(theZone, "badSound") then
cfxReconMode.badSound = cfxZones.getStringFromZoneProperty(theZone, "badSound", "<nosound>")
end
if cfxZones.hasProperty(theZone, "scoreSound") then
cfxReconMode.scoreSound = cfxZones.getStringFromZoneProperty(theZone, "scoreSound", "<nosound>")
end
end
function cfxPlayerScore.start()
if not dcsCommon.libCheck("cfx Player Score",
cfxPlayerScore.requiredLibs)
then
return false
end
-- read my score table
-- identify and process a score table zones
local theZone = cfxZones.getZoneByName("playerScoreTable")
if not theZone then
trigger.action.outText("+++scr: no score table!", 30)
else
-- read all into my types registry, replacing whatever is there
cfxPlayerScore.typeScore = cfxZones.getAllZoneProperties(theZone)
trigger.action.outText("+++scr: read score table", 30)
end
-- now read my config zone
local theZone = cfxZones.getZoneByName("playerScoreConfig")
if not theZone then
trigger.action.outText("+++scr: no config!", 30)
else
cfxPlayerScore.readConfigZone(theZone)
trigger.action.outText("+++scr: read config", 30)
end
-- subscribe to events and use dcsCommon's handler structure
dcsCommon.addEventHandler(cfxPlayerScore.killDetected,
cfxPlayerScore.preProcessor,
cfxPlayerScore.postProcessor)
trigger.action.outText("cfxPlayerScore v" .. cfxPlayerScore.version .. " started", 30)
return true
end
if not cfxPlayerScore.start() then
trigger.action.outText("+++ aborted cfxPlayerScore v" .. cfxPlayerScore.version .. " -- libcheck failed", 30)
cfxPlayerScore = nil
end
-- TODO: score mod for weapons type
-- TODO: player kill score

View File

@ -0,0 +1,303 @@
cfxPlayerScoreUI = {}
cfxPlayerScoreUI.version = "1.0.3"
--[[-- VERSION HISTORY
- 1.0.2 - initial version
- 1.0.3 - module check
--]]--
-- WARNING: REQUIRES cfxPlayerScore to work.
-- WARNING: ASSUMES SINGLE_PLAYER GROUPS!
cfxPlayerScoreUI.requiredLibs = {
"cfxPlayerScore", -- this is doing score keeping
"cfxPlayer", -- player events, comms
}
-- find & command cfxGroundTroops-based jtacs
-- UI installed via OTHER for all groups with players
-- module based on xxxGrpUI and jtacUI
cfxPlayerScoreUI.groupConfig = {} -- all inited group private config data
cfxPlayerScoreUI.simpleCommands = true -- if true, f10 other invokes directly
--
-- C O N F I G H A N D L I N G
-- =============================
--
-- Each group has their own config block that can be used to
-- store group-private data and configuration items.
--
function cfxPlayerScoreUI.resetConfig(conf)
end
function cfxPlayerScoreUI.createDefaultConfig(theGroup)
local conf = {}
conf.theGroup = theGroup
conf.name = theGroup:getName()
conf.id = theGroup:getID()
conf.coalition = theGroup:getCoalition()
cfxPlayerScoreUI.resetConfig(conf)
conf.mainMenu = nil; -- this is where we store the main menu if we branch
conf.myCommands = nil; -- this is where we store the commands if we branch
return conf
end
-- getConfigFor group will allocate if doesn't exist in DB
-- and add to it
function cfxPlayerScoreUI.getConfigForGroup(theGroup)
if not theGroup then
trigger.action.outText("+++WARNING: cfxPlayerScoreUI nil group in getConfigForGroup!", 30)
return nil
end
local theName = theGroup:getName()
local c = cfxPlayerScoreUI.getConfigByGroupName(theName) -- we use central accessor
if not c then
c = cfxPlayerScoreUI.createDefaultConfig(theGroup)
cfxPlayerScoreUI.groupConfig[theName] = c -- should use central accessor...
end
return c
end
function cfxPlayerScoreUI.getConfigByGroupName(theName) -- DOES NOT allocate when not exist
if not theName then return nil end
return cfxPlayerScoreUI.groupConfig[theName]
end
function cfxPlayerScoreUI.getConfigForUnit(theUnit)
-- simple one-off step by accessing the group
if not theUnit then
trigger.action.outText("+++WARNING: cfxPlayerScoreUI nil unit in getConfigForUnit!", 30)
return nil
end
local theGroup = theUnit:getGroup()
return getConfigForGroup(theGroup)
end
--
--
-- M E N U H A N D L I N G
-- =========================
--
--
function cfxPlayerScoreUI.clearCommsSubmenus(conf)
if conf.myCommands then
for i=1, #conf.myCommands do
missionCommands.removeItemForGroup(conf.id, conf.myCommands[i])
end
end
conf.myCommands = {}
end
function cfxPlayerScoreUI.removeCommsFromConfig(conf)
cfxPlayerScoreUI.clearCommsSubmenus(conf)
if conf.myMainMenu then
missionCommands.removeItemForGroup(conf.id, conf.myMainMenu)
conf.myMainMenu = nil
end
end
-- this only works in single-unit groups. may want to check if group
-- has disappeared
function cfxPlayerScoreUI.removeCommsForUnit(theUnit)
if not theUnit then return end
if not theUnit:isExist() then return end
-- perhaps add code: check if group is empty
local conf = cfxPlayerScoreUI.getConfigForUnit(theUnit)
cfxPlayerScoreUI.removeCommsFromConfig(conf)
end
function cfxPlayerScoreUI.removeCommsForGroup(theGroup)
if not theGroup then return end
if not theGroup:isExist() then return end
local conf = cfxPlayerScoreUI.getConfigForGroup(theGroup)
cfxPlayerScoreUI.removeCommsFromConfig(conf)
end
--
-- set main root in F10 Other. All sub menus click into this
--
--function cfxPlayerScoreUI.isEligibleForMenu(theGroup)
-- return true
--end
function cfxPlayerScoreUI.setCommsMenuForUnit(theUnit)
if not theUnit then
trigger.action.outText("+++WARNING: cfxPlayerScoreUI nil UNIT in setCommsMenuForUnit!", 30)
return
end
if not theUnit:isExist() then return end
local theGroup = theUnit:getGroup()
cfxPlayerScoreUI.setCommsMenu(theGroup)
end
function cfxPlayerScoreUI.setCommsMenu(theGroup)
-- depending on own load state, we set the command structure
-- it begins at 10-other, and has 'grpUI' as main menu with submenus
-- as required
if not theGroup then return end
if not theGroup:isExist() then return end
-- we test here if this group qualifies for
-- the menu. if not, exit
--if not cfxPlayerScoreUI.isEligibleForMenu(theGroup) then return end
local conf = cfxPlayerScoreUI.getConfigForGroup(theGroup)
conf.id = theGroup:getID(); -- we do this ALWAYS so it is current even after a crash
if cfxPlayerScoreUI.simpleCommands then
-- we install directly in F-10 other
if not conf.myMainMenu then
local commandTxt = "Score / Kills"
local theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
nil,
cfxPlayerScoreUI.redirectCommandX,
{conf, "score"}
)
conf.myMainMenu = theCommand
end
return
end
-- ok, first, if we don't have an F-10 menu, create one
if not (conf.myMainMenu) then
conf.myMainMenu = missionCommands.addSubMenuForGroup(conf.id, 'Score / Kills')
end
-- clear out existing commands
cfxPlayerScoreUI.clearCommsSubmenus(conf)
-- now we have a menu without submenus.
-- add our own submenus
cfxPlayerScoreUI.addSubMenus(conf)
end
function cfxPlayerScoreUI.addSubMenus(conf)
-- add menu items to choose from after
-- user clickedf on MAIN MENU. In this implementation
-- they all result invoked methods
local commandTxt = "Show Score / Kills"
local theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myMainMenu,
cfxPlayerScoreUI.redirectCommandX,
{conf, "score"}
)
table.insert(conf.myCommands, theCommand)
end
--
-- each menu item has a redirect and timed invoke to divorce from the
-- no-debug zone in the menu invocation. Delay is .1 seconds
--
function cfxPlayerScoreUI.redirectCommandX(args)
timer.scheduleFunction(cfxPlayerScoreUI.doCommandX, args, timer.getTime() + 0.1)
end
function cfxPlayerScoreUI.doCommandX(args)
local conf = args[1] -- < conf in here
local what = args[2] -- < second argument in here
local theGroup = conf.theGroup
-- now fetch the first player that drives a unit in this group
-- a simpler method would be to access conf.primeUnit
local playerName, playerUnit = cfxPlayer.getFirstGroupPlayerName(theGroup)
if playerName == nil or playerUnit == nil then
trigger.action.outText("scoreUI: nil player name or unit for group " .. theGroup:getName(), 30)
return
end
local desc = cfxPlayerScore.scoreTextForPlayerNamed(playerName)
trigger.action.outTextForGroup(conf.id, desc, 30)
trigger.action.outSoundForGroup(conf.id, "Quest Snare 3.wav")
end
--
-- G R O U P M A N A G E M E N T
--
-- Group Management is required to make sure all groups
-- receive a comms menu and that they receive a clean-up
-- when required
--
-- Callbacks are provided by cfxPlayer module to which we
-- subscribe during init
--
function cfxPlayerScoreUI.playerChangeEvent(evType, description, player, data)
if evType == "newGroup" then
cfxPlayerScoreUI.setCommsMenu(data.group)
return
end
if evType == "removeGroup" then
-- we must remove the comms menu for this group else we try to add another one to this group later
local conf = cfxPlayerScoreUI.getConfigByGroupName(data.name)
if conf then
cfxPlayerScoreUI.removeCommsFromConfig(conf) -- remove menus
cfxPlayerScoreUI.resetConfig(conf) -- re-init this group for when it re-appears
else
trigger.action.outText("+++ scoreUI: can't retrieve group <" .. data.name .. "> config: not found!", 30)
end
return
end
end
--
-- Start
--
function cfxPlayerScoreUI.start()
if not dcsCommon.libCheck("cfx PlayerScoreUI",
cfxPlayerScoreUI.requiredLibs)
then
return false
end
-- iterate existing groups so we have a start situation
-- 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 player record. Does not resolve on unit level!
for gname, pgroup in pairs(allPlayerGroups) do
local theUnit = pgroup.primeUnit -- get any unit of that group
cfxPlayerScoreUI.setCommsMenuForUnit(theUnit) -- set up
end
-- now install the new group notifier to install Assault Troops menu
cfxPlayer.addMonitor(cfxPlayerScoreUI.playerChangeEvent)
trigger.action.outText("cf/x cfxPlayerScoreUI v" .. cfxPlayerScoreUI.version .. " started", 30)
return true
end
--
-- GO GO GO
--
if not cfxPlayerScoreUI.start() then
cfxPlayerScoreUI = nil
trigger.action.outText("cf/x PlayerScore UI aborted: missing libraries", 30)
end

385
modules/cfxReconGUI.lua Normal file
View File

@ -0,0 +1,385 @@
cfxReconGUI = {}
cfxReconGUI.version = "1.0.0"
--[[-- VERSION HISTORY
- 1.0.0 - initial version
--]]--
-- find & command cfxGroundTroops-based jtacs
-- UI installed via OTHER for all groups with players
-- module based on xxxGrpUI
cfxReconGUI.groupConfig = {} -- all inited group private config data
cfxReconGUI.simpleCommands = true -- if true, f10 other invokes directly
--
-- C O N F I G H A N D L I N G
-- =============================
--
-- Each group has their own config block that can be used to
-- store group-private data and configuration items.
--
function cfxReconGUI.resetConfig(conf)
if conf.scouting then
-- after a crash or other, reset
cfxReconMode.removeScout(conf.unit)
trigger.action.outTextForGroup(conf.id, "Lost contact to scout...", 30)
end
conf.scouting = false -- if true, we are currently scouting.
end
function cfxReconGUI.createDefaultConfig(theGroup)
local conf = {}
conf.theGroup = theGroup
conf.name = theGroup:getName()
conf.id = theGroup:getID()
conf.coalition = theGroup:getCoalition()
local groupUnits = theGroup:getUnits()
conf.unit = groupUnits[1] -- WARNING: ASSUMES ONE-UNIT GROUPS
cfxReconGUI.resetConfig(conf)
conf.mainMenu = nil; -- this is where we store the main menu if we branch
conf.myCommands = nil; -- this is where we store the commands if we branch
return conf
end
-- getConfigFor group will allocate if doesn't exist in DB
-- and add to it
function cfxReconGUI.getConfigForGroup(theGroup)
if not theGroup then
trigger.action.outText("+++WARNING: cfxReconGUI nil group in getConfigForGroup!", 30)
return nil
end
local theName = theGroup:getName()
local c = cfxReconGUI.getConfigByGroupName(theName) -- we use central accessor
if not c then
c = cfxReconGUI.createDefaultConfig(theGroup)
cfxReconGUI.groupConfig[theName] = c -- should use central accessor...
end
return c
end
function cfxReconGUI.getConfigByGroupName(theName) -- DOES NOT allocate when not exist
if not theName then return nil end
return cfxReconGUI.groupConfig[theName]
end
function cfxReconGUI.getConfigForUnit(theUnit)
-- simple one-off step by accessing the group
if not theUnit then
trigger.action.outText("+++WARNING: cfxReconGUI nil unit in getConfigForUnit!", 30)
return nil
end
local theGroup = theUnit:getGroup()
local conf = getConfigForGroup(theGroup)
conf.unit = theUnit
return conf
end
--
--
-- M E N U H A N D L I N G
-- =========================
--
--
function cfxReconGUI.clearCommsSubmenus(conf)
if conf.myCommands then
for i=1, #conf.myCommands do
missionCommands.removeItemForGroup(conf.id, conf.myCommands[i])
end
end
conf.myCommands = {}
end
function cfxReconGUI.removeCommsFromConfig(conf)
cfxReconGUI.clearCommsSubmenus(conf)
if conf.myMainMenu then
missionCommands.removeItemForGroup(conf.id, conf.myMainMenu)
conf.myMainMenu = nil
end
end
-- this only works in single-unit groups. may want to check if group
-- has disappeared
function cfxReconGUI.removeCommsForUnit(theUnit)
if not theUnit then return end
if not theUnit:isExist() then return end
-- perhaps add code: check if group is empty
local conf = cfxReconGUI.getConfigForUnit(theUnit)
cfxReconGUI.removeCommsFromConfig(conf)
end
function cfxReconGUI.removeCommsForGroup(theGroup)
if not theGroup then return end
if not theGroup:isExist() then return end
local conf = cfxReconGUI.getConfigForGroup(theGroup)
cfxReconGUI.removeCommsFromConfig(conf)
end
--
-- set main root in F10 Other. All sub menus click into this
--
function cfxReconGUI.isEligibleForMenu(theGroup)
return true
end
function cfxReconGUI.setCommsMenuForUnit(theUnit)
if not theUnit then
trigger.action.outText("+++WARNING: cfxReconGUI nil UNIT in setCommsMenuForUnit!", 30)
return
end
if not theUnit:isExist() then
trigger.action.outText("+++WARNING: cfxReconGUI unit:ISEXIST() failed in setCommsMenuForUnit!", 30)
return
end
local theGroup = theUnit:getGroup()
cfxReconGUI.setCommsMenu(theGroup)
end
function cfxReconGUI.setCommsMenu(theGroup)
-- depending on own load state, we set the command structure
-- it begins at 10-other, and has 'jtac' as main menu with submenus
-- as required
if not theGroup then return end
if not theGroup:isExist() then return end
-- we test here if this group qualifies for
-- the menu. if not, exit
if not cfxReconGUI.isEligibleForMenu(theGroup) then return end
local conf = cfxReconGUI.getConfigForGroup(theGroup)
conf.id = theGroup:getID(); -- we do this ALWAYS so it is current even after a crash
-- trigger.action.outText("+++ setting group <".. conf.theGroup:getName() .. "> jtac command", 30)
if cfxReconGUI.simpleCommands then
-- we install directly in F-10 other
if not conf.myMainMenu then
local commandTxt = "Recon: "
local unitName = "bogus"
if conf.unit and conf.unit:isExist() then
unitName = conf.unit:getName()
elseif conf.unit then
trigger.action.outText("+++Recon: ISEXIST failed for unit in comms setup!", 30)
commandTxt = commandTxt .. "***"
else
trigger.action.outText("+++Recon: NIL unit in comms setup!", 30)
commandTxt = commandTxt .. "***"
end
if conf.scouting then
commandTxt = commandTxt .. " Stop Reporting"
else
commandTxt = commandTxt .. " Commence Reports"
end
local theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
nil,
cfxReconGUI.redirectCommandX,
{conf, "recon", unitName}
)
conf.myMainMenu = theCommand
end
return
end
-- ok, first, if we don't have an F-10 menu, create one
if not (conf.myMainMenu) then
conf.myMainMenu = missionCommands.addSubMenuForGroup(conf.id, 'Recon')
end
-- clear out existing commands
cfxReconGUI.clearCommsSubmenus(conf)
-- now we have a menu without submenus.
-- add our own submenus
cfxReconGUI.addSubMenus(conf)
end
function cfxReconGUI.addSubMenus(conf)
-- add menu items to choose from after
-- user clickedf on MAIN MENU. In this implementation
-- they all result invoked methods
local commandTxt = "Recon"
local unitName = "bogus"
if conf.unit and conf.unit:getName()then
unitName = conf.unit:getName()
else
trigger.action.outTextForCoalition("+++Recon: no unit in comms setup!", message, 30)
commandTxt = commandTxt .. "***"
end
local theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myMainMenu,
cfxReconGUI.redirectCommandX,
{conf, "recon", unitName}
)
table.insert(conf.myCommands, theCommand)
--[[--
commandTxt = "This is another important command"
theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myMainMenu,
cfxReconGUI.redirectCommandX,
{conf, "Sub2"}
)
table.insert(conf.myCommands, theCommand)
--]]--
end
--
-- each menu item has a redirect and timed invoke to divorce from the
-- no-debug zone in the menu invocation. Delay is .1 seconds
--
function cfxReconGUI.redirectCommandX(args)
timer.scheduleFunction(cfxReconGUI.doCommandX, args, timer.getTime() + 0.1)
end
function cfxReconGUI.doCommandX(args)
local conf = args[1] -- < conf in here
local what = args[2] -- < second argument in here
local unitName = args[3]
if not unitName then
trigger.action.outText("+++ reconUI: doCommand: UNDEF unitName!", 30)
return
elseif unitName == "bogus" then
trigger.action.outText("+++ reconUI: doCommand: BOGUS unitName!", 30)
end
local theGroup = conf.theGroup
-- trigger.action.outTextForGroup(conf.id, "+++ groupUI: processing comms menu for <" .. what .. ">", 30)
-- whenever we get here, we toggle the recon mode
local theUnit = conf.unit
local message = "Scout ".. unitName .. " has stopped reporting."
local theSide = conf.coalition
if conf.scouting then
-- end recon
cfxReconMode.removeScoutByName(unitName)
if theUnit:isExist() then
message = theUnit:getName() .. " folds map, recon terminated."
end
conf.scouting = false
else
-- start recon
if theUnit and theUnit:isExist() then
cfxReconMode.addScout(theUnit)
message = theUnit:getName() .. " reports bright eyes, commencing recon."
conf.scouting = true
else
message = "+++ reconGUI: " .. unitName .. " has invalid unit"
end
end
trigger.action.outTextForCoalition(theSide, message, 30)
-- reset comms
cfxReconGUI.removeCommsForGroup(theGroup)
cfxReconGUI.setCommsMenu(theGroup)
end
--
-- G R O U P M A N A G E M E N T
--
-- Group Management is required to make sure all groups
-- receive a comms menu and that they receive a clean-up
-- when required
--
-- Callbacks are provided by cfxPlayer module to which we
-- subscribe during init
--
function cfxReconGUI.playerChangeEvent(evType, description, player, data)
--trigger.action.outText("+++ groupUI: received <".. evType .. "> Event", 30)
if evType == "newGroup" then
-- initialized attributes are in data as follows
-- .group - new group
-- .name - new group's name
-- .primeUnit - the unit that trigggered new group appearing
-- .primeUnitName - name of prime unit
-- .id group ID
--theUnit = data.primeUnit
-- ensure group data exists and is updated
local conf = cfxReconGUI.getConfigForGroup(data.group)
conf.unit = data.primeUnit
conf.unitName = conf.unit:getName() -- will break if no exist
cfxReconGUI.setCommsMenu(data.group)
-- trigger.action.outText("+++ groupUI: added " .. theUnit:getName() .. " to comms menu", 30)
return
end
if evType == "removeGroup" then
-- data is the player record that no longer exists. it consists of
-- .name
-- we must remove the comms menu for this group else we try to add another one to this group later
local conf = cfxReconGUI.getConfigByGroupName(data.name)
if conf then
cfxReconGUI.removeCommsFromConfig(conf) -- remove menus
cfxReconGUI.resetConfig(conf) -- re-init this group for when it re-appears
else
trigger.action.outText("+++ reconUI: can't retrieve group <" .. data.name .. "> config: not found!", 30)
end
return
end
if evType == "leave" then
-- player unit left. we don't care since we only work on group level
-- if they were the only, this is followed up by group disappeared
end
if evType == "unit" then
-- player changed units. almost never in MP, but possible in solo
-- because of 1 seconds timing loop
-- will result in a new group appearing and a group disappearing, so we are good
-- may need some logic to clean up old configs and/or menu items
end
end
--
-- Start
--
function cfxReconGUI.start()
-- iterate existing groups so we have a start situation
-- 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 player record. Does not resolve on unit level!
for gname, pgroup in pairs(allPlayerGroups) do
local theUnit = pgroup.primeUnit -- get any unit of that group
cfxReconGUI.setCommsMenuForUnit(theUnit) -- set up
end
-- now install the new group notifier to install Assault Troops menu
cfxPlayer.addMonitor(cfxReconGUI.playerChangeEvent)
trigger.action.outText("cf/x cfxReconGUI v" .. cfxReconGUI.version .. " started", 30)
end
--
-- GO GO GO
--
if not cfxReconMode then
trigger.action.outText("cf/x cfxReconGUI REQUIRES cfxReconMode to work.", 30)
else
cfxReconGUI.start()
end

606
modules/cfxReconMode.lua Normal file
View File

@ -0,0 +1,606 @@
cfxReconMode = {}
cfxReconMode.version = "1.4.1"
cfxReconMode.verbose = false -- set to true for debug info
cfxReconMode.reconSound = "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" -- to be played when somethiong discovered
cfxReconMode.prioList = {} -- group names that are high prio and generate special event
cfxReconMode.blackList = {} -- group names that are NEVER detected. Comma separated strings, e.g. {"Always Hidden", "Invisible Group"}
cfxReconMode.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
}
--[[--
VERSION HISTORY
1.0.0 - initial version
1.0.1 - removeScoutByName()
1.0.2 - garbage collection
1.1.0 - autoRecon - any aircraft taking off immediately
signs up, no message when signing up or closing down
standalone - copied common procs lerp, agl, dist, distflat
from dcsCommon
report numbers
verbose flag
1.2.0 - queued recons. One scout per second for more even
performance
removed gc since it's now integrated into
update queue
removeScout optimization when directly passing name
playerOnlyRecon for autoRecon
red, blue, grey side filtering on auto scout
1.2.1 - parametrized report sound
1.3.0 - added black list, prio list functionality
1.3.1 - callbacks now also push name, as group can be dead
- removed bug when removing dead groups from map
1.4.0 - import dcsCommon, cfxZones etc
- added lib check
- config zone
- prio+
- detect+
1.4.1 - invocation no longer happen twice for prio.
- recon sound
- read all flight groups at start to get rid of the
- late activation work-around
cfxReconMode is a script that allows units to perform reconnaissance
missions and, after detecting units, marks them on the map with
markers for their coalition and some text
Also, a callback is initiated for scouts as follows
signature: (reason, theSide, theSout, theGroup) with
reason a string
'detected' a group was detected
'removed' a mark for a group timed out
'priority' a member of prio group was detected
'start' a scout started scouting
'end' a scout stopped scouting
'dead' a scout has died and was removed from pool
theSide - side of the SCOUT that detected units
theScout - the scout that detected the group
theGroup - the group that is detected
theName - the group's name
--]]--
cfxReconMode.detectionMinRange = 3000 -- meters at ground level
cfxReconMode.detectionMaxRange = 12000 -- meters at max alt (10'000m)
cfxReconMode.maxAlt = 9000 -- alt for maxrange (9km = 27k feet)
cfxReconMode.autoRecon = true -- add all airborne units, unless
cfxReconMode.redScouts = false -- set to false to prevent red scouts in auto mode
cfxReconMode.blueScouts = true -- set to false to prevent blue scouts in auto-mode
cfxReconMode.greyScouts = false -- set to false to prevent neutral scouts in auto mode
cfxReconMode.playerOnlyRecon = false -- only players can do recon
cfxReconMode.reportNumbers = true -- also add unit count in report
cfxReconMode.prioFlag = nil
cfxReconMode.detectFlag = nil
cfxReconMode.applyMarks = true
cfxReconMode.ups = 1 -- updates per second.
cfxReconMode.scouts = {} -- units that are performing scouting.
cfxReconMode.processedScouts = {} -- for managing performance: queue
cfxReconMode.detectedGroups = {} -- so we know which have been detected
cfxReconMode.marksFadeAfter = 30*60 -- after detection, marks disappear after
-- this amount of seconds. -1 means no fade
-- 60 is one minute
cfxReconMode.callbacks = {} -- sig: cb(reason, side, scout, group)
cfxReconMode.uuidCount = 0 -- for unique marks
-- end standalone dcsCommon extract
function cfxReconMode.uuid()
cfxReconMode.uuidCount = cfxReconMode.uuidCount + 1
return cfxReconMode.uuidCount
end
function cfxReconMode.addCallback(theCB)
table.insert(cfxReconMode.callbacks, theCB)
end
function cfxReconMode.invokeCallbacks(reason, theSide, theScout, theGroup, theName)
for idx, theCB in pairs(cfxReconMode.callbacks) do
theCB(reason, theSide, theScout, theGroup, theName)
end
end
-- add a priority/blackList group name to prio list
function cfxReconMode.addToPrioList(aGroup)
if not aGroup then return end
if type(aGroup) == "table" and aGroup.getName then
aGroup = aGroup:getName()
end
if type(aGroup) == "string" then
table.insert(cfxReconMode.prioList, aGroup)
end
end
function cfxReconMode.addToBlackList(aGroup)
if not aGroup then return end
if type(aGroup) == "table" and aGroup.getName then
aGroup = aGroup:getName()
end
if type(aGroup) == "string" then
table.insert(cfxReconMode.blackList, aGroup)
end
end
function cfxReconMode.isStringInList(theString, theList)
if not theString then return false end
if not theList then return false end
if type(theString) == "string" then
for idx,anItem in pairs(theList) do
if anItem == theString then return true end
end
end
return false
end
-- addScout directly adds a scout unit. Use from external
-- to manually add a unit (e.g. via GUI when autoscout isExist
-- off, or to force a scout unit (e.g. when scouts for a side
-- are not allowed but you still want a unit from that side
-- to scout
-- since we use a queue for scouts, also always check the
-- processed queue before adding to make sure a scout isn't
-- entered multiple times
function cfxReconMode.addScout(theUnit)
if not theUnit then
trigger.action.outText("+++cfxRecon: WARNING - nil Unit on add", 30)
return
end
if type(theUnit) == "string" then
local u = Unit.getByName(theUnit)
theUnit = u
end
if not theUnit then
trigger.action.outText("+++cfxRecon: WARNING - did not find unit on add", 30)
return
end
if not theUnit:isExist() then return end
-- find out if this an update or a new scout
local thisID = tonumber(theUnit:getID())
local theName = theUnit:getName()
local lastUnit = cfxReconMode.scouts[theName]
local isProcced = false -- may also be in procced line
if not lastUnit then
lastUnit = cfxReconMode.processedScouts[theName]
if lastUnit then isProcced = true end
end
if lastUnit then
-- this is merely an overwrite
if cfxReconMode.verbose then trigger.action.outText("+++rcn: UPDATE scout " .. theName .. " -- no CB invoke", 30) end
else
if cfxReconMode.verbose then trigger.action.outText("+++rcn: new scout " .. theName .. " with ID " .. thisID, 30) end
-- a new scout! Invoke callbacks
local scoutGroup = theUnit:getGroup()
local theSide = scoutGroup:getCoalition()
cfxReconMode.invokeCallbacks("start", theSide, theUnit, nil, "<none>")
end
if isProcced then
-- overwrite exiting entry in procced queue
cfxReconMode.processedScouts[theName] = theUnit
else
-- add / overwrite into normal queue
cfxReconMode.scouts[theName] = theUnit
end
if cfxReconMode.verbose then
trigger.action.outText("+++rcn: addded scout " .. theUnit:getName(), 30)
end
end
function cfxReconMode.removeScout(theUnit)
if not theUnit then
trigger.action.outText("+++cfxRecon: WARNING - nil Unit on remove", 30)
return
end
if type(theUnit) == "string" then
cfxReconMode.removeScoutByName(theUnit)
return
end
if not theUnit then return end
if not theUnit:isExist() then return end
cfxReconMode.removeScoutByName(theUnit:getName())
local scoutGroup = theUnit:getGroup()
local theSide = scoutGroup:getCoalition()
cfxReconMode.invokeCallbacks("end", theSide, theUnit, nil, "<none>")
end
-- warning: removeScoutByName does NOT invoke callbacks, always
-- use removeScout instead!
function cfxReconMode.removeScoutByName(aName)
cfxReconMode.scouts[aName] = nil
cfxReconMode.processedScouts[aName] = nil -- also remove from processed stack
if cfxReconMode.verbose then
trigger.action.outText("+++rcn: removed scout " .. aName, 30)
end
end
function cfxReconMode.canDetect(scoutPos, theGroup, visRange)
-- determine if a member of theGroup can be seen from
-- scoutPos at visRange
-- returns true and pos when detected
local allUnits = theGroup:getUnits()
for idx, aUnit in pairs(allUnits) do
if aUnit:isExist() and aUnit:getLife() >= 1 then
local uPos = aUnit:getPoint()
uPos.y = uPos.y + 3 -- raise my 3 meters
local d = dcsCommon.distFlat(scoutPos, uPos)
if d < visRange then
-- is in visual range. do we have LOS?
if land.isVisible(scoutPos, uPos) then
-- group is visible, stop here, return true
return true, uPos
end
else
-- OPTIMIZATION: if a unit is outside
-- detect range, we assume that entire group
-- is, since they are bunched together
-- edge cases may get lucky tests
return false, nil
end
end
end
return false, nil -- nothing visible
end
function cfxReconMode.placeMarkForUnit(location, theSide, theGroup)
local theID = cfxReconMode.uuid()
local theDesc = "Contact: "..theGroup:getName()
if cfxReconMode.reportNumbers then
theDesc = theDesc .. " (" .. theGroup:getSize() .. " units)"
end
trigger.action.markToCoalition(
theID,
theDesc,
location,
theSide,
false,
nil)
return theID
end
function cfxReconMode.removeMarkForArgs(args)
local theSide = args[1]
local theScout = args[2]
local theGroup = args[3]
local theID = args[4]
local theName = args[5]
-- if not theGroup then return end
-- if not theGroup:isExist then return end
trigger.action.removeMark(theID)
cfxReconMode.detectedGroups[theName] = nil
-- invoke callbacks
cfxReconMode.invokeCallbacks("removed", theSide, theScout, theGroup, theName)
end
function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc)
-- put a mark on the map
if cfxReconMode.applyMarks then
local theID = cfxReconMode.placeMarkForUnit(theLoc, mySide, theGroup)
-- schedule removal if desired
if cfxReconMode.marksFadeAfter > 0 then
args = {mySide, theScout, theGroup, theID, theGroup:getName()}
timer.scheduleFunction(cfxReconMode.removeMarkForArgs, args, timer.getTime() + cfxReconMode.marksFadeAfter)
end
end
-- say something
if cfxReconMode.announcer then
trigger.action.outTextForCoalition(mySide, theScout:getName() .. " reports new ground contact " .. theGroup:getName(), 30)
-- play a sound
trigger.action.outSoundForCoalition(mySide, cfxReconMode.reconSound)
end
-- see if it was a prio target
if cfxReconMode.isStringInList(theGroup:getName(), cfxReconMode.prioList) then
if cfxReconMode.announcer then
trigger.action.outTextForCoalition(mySide, "Priority target confirmed", 30)
end
-- invoke callbacks
cfxReconMode.invokeCallbacks("priotity", mySide, theScout, theGroup, theGroup:getName())
-- increase prio flag
if cfxReconMode.prioFlag then
local currVal = trigger.misc.getUserFlag(cfxReconMode.prioFlag)
trigger.action.setUserFlag(cfxReconMode.prioFlag, currVal + 1)
end
else
-- invoke callbacks
cfxReconMode.invokeCallbacks("detected", mySide, theScout, theGroup, theGroup:getName())
-- increase normal flag
if cfxReconMode.detectFlag then
local currVal = trigger.misc.getUserFlag(cfxReconMode.detectFlag)
trigger.action.setUserFlag(cfxReconMode.detectFlag, currVal + 1)
end
end
end
function cfxReconMode.performReconForUnit(theScout)
if not theScout then return end
if not theScout:isExist() then return end -- will be gc'd soon
-- get altitude above ground to calculate visual range
local alt = dcsCommon.getUnitAGL(theScout)
local visRange = dcsCommon.lerp(cfxReconMode.detectionMinRange, cfxReconMode.detectionMaxRange, alt/cfxReconMode.maxAlt)
local scoutPos = theScout:getPoint()
-- figure out which groups we are looking for
local myCoal = theScout:getCoalition()
local enemyCoal = 1
if myCoal == 1 then enemyCoal = 2 end
-- iterate all enemy units until we find one
-- and then stop this iteration (can only detect one
-- group per pass)
local enemyGroups = coalition.getGroups(enemyCoal)
for idx, theGroup in pairs (enemyGroups) do
-- make sure it's a ground unit
local isGround = theGroup:getCategory() == 2
if theGroup:isExist() and isGround then
local visible, location = cfxReconMode.canDetect(scoutPos, theGroup, visRange)
if visible then
-- see if we already detected this one
local groupName = theGroup:getName()
if cfxReconMode.detectedGroups[groupName] == nil then
-- only now check against blackList
if not cfxReconMode.isStringInList(groupName, cfxReconMode.blackList) then
-- visible and not yet seen
-- perhaps add some percent chance now
-- remember that we know this group
cfxReconMode.detectedGroups[groupName] = theGroup
cfxReconMode.detectedGroup(myCoal, theScout, theGroup, location)
return -- stop, as we only detect one group per pass
end
end
end
end
end
end
function cfxReconMode.updateQueues()
-- schedule next call
timer.scheduleFunction(cfxReconMode.updateQueues, {}, timer.getTime() + 1/cfxReconMode.ups)
-- we only process the first aircraft in
-- the scouts array, move it to processed and then shrink
-- scouts table until it's empty. When empty, transfer all
-- back and start cycle anew
local theFocusScoutName = nil
local procCount = 0 -- no iterations done yet
for name, scout in pairs(cfxReconMode.scouts) do
theFocusScoutName = name -- remember so we can delete
if not scout:isExist() then
-- we ignore the scout, and it's
-- forgotten since no longer transferred
-- i.e. built-in GC
if cfxReconMode.verbose then
trigger.action.outText("+++rcn: GC - removing scout " .. name .. " because it no longer exists", 30)
end
-- invoke 'end' for this scout
cfxReconMode.invokeCallbacks("dead", -1, nil, nil, name)
else
-- scan for this scout
cfxReconMode.performReconForUnit(scout)
-- move it to processed table
cfxReconMode.processedScouts[name] = scout
end
procCount = 1 -- remember we went through one iteration
break -- always end after first iteration
end
-- remove processed scouts from scouts array
if procCount > 0 then
-- we processed one scout (even if scout itself did not exist)
-- remove that scout from active scouts table
cfxReconMode.scouts[theFocusScoutName] = nil
else
-- scouts is empty. copy processed table back to scouts
-- restart scouts array, contains GC already
cfxReconMode.scouts = cfxReconMode.processedScouts
cfxReconMode.processedScouts = {} -- start new empty processed queue
end
end
-- event handler
function cfxReconMode:onEvent(event)
if not event then return end
if not event.initiator then return end
local theUnit = event.initiator
-- we simply add scouts as they are garbage-collected
-- every so often when they do not exist
if event.id == 15 or -- birth
event.id == 3 -- take-off. should already have been taken
-- care of by birth, but you never know
then
-- check if a side must not have scouts.
-- this will prevent player units to auto-
-- scout when they are on that side. in that case
-- you must add manually
local theSide = theUnit:getCoalition()
if theSide == 0 and not cfxReconMode.greyScouts then
return -- grey scouts are not allowed
end
if theSide == 1 and not cfxReconMode.redScouts then
return -- grey scouts are not allowed
end
if theSide == 2 and not cfxReconMode.blueScouts then
return -- grey scouts are not allowed
end
if cfxReconMode.playerOnlyRecon then
if not theUnit:getPlayerName() then
return -- only players can do recon. this unit is AI
end
end
if cfxReconMode.verbose then
trigger.action.outText("+++rcn: event " .. event.id .. " for unit " .. theUnit:getName(), 30)
end
cfxReconMode.addScout(theUnit)
end
end
--
-- read all existing planes
--
function cfxReconMode.processScoutGroups(theGroups)
for idx, aGroup in pairs(theGroups) do
-- process all planes in that group
-- we are very early in the mission, only few groups really
-- exist now, the rest of the units come in with 15 event
if aGroup:isExist() then
local allUnits = Group.getUnits(aGroup)
for idy, aUnit in pairs (allUnits) do
if aUnit:isExist() then
cfxReconMode.addScout(aUnit)
if cfxReconMode.verbose then
trigger.action.outText("+++rcn: added unit " ..aUnit:getName() .. " to pool at startup", 30)
end
end
end
end
end
end
function cfxReconMode.initScouts()
-- get all groups of aircraft. Unrolled loop 0..2
local theAirGroups = {}
if cfxReconMode.greyScouts then
theAirGroups = coalition.getGroups(0, 0) -- 0 = aircraft
cfxReconMode.processScoutGroups(theAirGroups)
end
if cfxReconMode.redScouts then
theAirGroups = coalition.getGroups(1, 0) -- 1 = red, 0 = aircraft
cfxReconMode.processScoutGroups(theAirGroups)
end
if cfxReconMode.blueScouts then
theAirGroups = coalition.getGroups(2, 0) -- 2 = blue, 0 = aircraft
cfxReconMode.processScoutGroups(theAirGroups)
end
end
--
-- read config
--
function cfxReconMode.readConfigZone()
-- note: must match exactly!!!!
local theZone = cfxZones.getZoneByName("reconModeConfig")
if not theZone then
if cfxReconMode.verbose then
trigger.action.outText("+++rcn: no config zone!", 30)
end
return
end
if cfxReconMode.verbose then
trigger.action.outText("+++rcn: found config zone!", 30)
end
cfxReconMode.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
cfxReconMode.autoRecon = cfxZones.getBoolFromZoneProperty(theZone, "autoRecon", true)
cfxReconMode.redScouts = cfxZones.getBoolFromZoneProperty(theZone, "redScouts", false)
cfxReconMode.blueScouts = cfxZones.getBoolFromZoneProperty(theZone, "blueScouts", true)
cfxReconMode.greyScouts = cfxZones.getBoolFromZoneProperty(theZone, "greyScouts", false)
cfxReconMode.playerOnlyRecon = cfxZones.getBoolFromZoneProperty(theZone, "playerOnlyRecon", false)
cfxReconMode.reportNumbers = cfxZones.getBoolFromZoneProperty(theZone, "reportNumbers", true)
cfxReconMode.detectionMinRange = cfxZones.getNumberFromZoneProperty(theZone, "detectionMinRange", 3000)
cfxReconMode.detectionMaxRange = cfxZones.getNumberFromZoneProperty(theZone, "detectionMaxRange", 12000)
cfxReconMode.maxAlt = cfxZones.getNumberFromZoneProperty(theZone, "maxAlt", 9000)
if cfxZones.hasProperty(theZone, "prio+") then
cfxReconMode.prioFlag = cfxZones.getStringFromZoneProperty(theZone, "prio+", "none")
end
if cfxZones.hasProperty(theZone, "detect+") then
cfxReconMode.detectFlag = cfxZones.getStringFromZoneProperty(theZone, "detect+", "none")
end
cfxReconMode.applyMarks = cfxZones.getBoolFromZoneProperty(theZone, "applyMarks", true)
cfxReconMode.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true)
if cfxZones.hasProperty(theZone, "reconSound") then
cfxReconMode.reconSound = cfxZones.getStringFromZoneProperty(theZone, "reconSound", "<nosound>")
end
end
--
-- start
--
function cfxReconMode.start()
-- lib check
if not dcsCommon.libCheck("cfx Recon Mode",
cfxReconMode.requiredLibs) then
return false
end
-- read config
cfxReconMode.readConfigZone()
-- gather exiting planes
cfxReconMode.initScouts()
-- start update cycle
cfxReconMode.updateQueues()
if cfxReconMode.autoRecon then
-- install own event handler to detect
-- when a unit takes off and add it to scout
-- roster
world.addEventHandler(cfxReconMode)
end
trigger.action.outText("cfx Recon version " .. cfxReconMode.version .. " started.", 30)
return true
end
--
-- test callback
--
function cfxReconMode.demoReconCB(reason, theSide, theScout, theGroup, theName)
trigger.action.outText("recon CB: " .. reason .. " -- " .. theScout:getName() .. " spotted " .. theName, 30)
end
if not cfxReconMode.start() then
cfxReconMode = nil
end
-- debug: wire up my own callback
-- cfxReconMode.addCallback(cfxReconMode.demoReconCB)
--[[--
ideas:
- renew lease. when already sighted, simply renew lease, maybe update location.
- update marks and renew lease
TODO: red+ and blue+ - flags to increase when a plane of the other side is detected
--]]--

470
modules/cfxSSBClient.lua Normal file
View File

@ -0,0 +1,470 @@
cfxSSBClient = {}
cfxSSBClient.version = "2.0.0"
cfxSSBClient.verbose = false
cfxSSBClient.singleUse = false -- set to true to block crashed planes
-- NOTE: singleUse (true) requires SSB to disable immediate respawn after kick
cfxSSBClient.reUseAfter = -1 -- seconds for re-use delay
-- only when singleUse is in effect. -1 means never
cfxSSBClient.requiredLibs = {
"dcsCommon", -- always
"cfxGroups", -- for slot access
"cfxZones", -- Zones, of course
}
--[[--
Version History
1.0.0 - initial version
1.1.0 - detect airfield by action and location, not group name
1.1.1 - performance tuning. only read player groups once
- and remove in-air-start groups from scan. this requires
- ssb (server) be not modified
1.2.0 - API to close airfields: invoke openAirfieldNamed()
and closeAirfieldNamed() with name as string (exact match required)
to block an airfield for any player aircraft.
Works for FARPS as well
API to associate a player group with any airfied's status (nil for unbind):
cfxSSBClient.bindGroupToAirfield(group, airfieldName)
API shortcut to unbind groups: cfxSSBClient.unbindGroup(group)
verbose messages now identify better: "+++SSB:"
keepInAirGroups option
2.0.0 - include single-use ability: crashed airplanes are blocked from further use
- single-use can be turned off
- getPlayerGroupForGroupNamed()
- split setSlotAccess to single accessor
and interator
- reUseAfter option for single-use
- dcsCommon, cfxZones import
WHAT IT IS
SSB Client is a small script that forms the client-side counterpart to
Ciribob's simple slot block. It will block slots for all client airframes
that are on an airfield that does not belong to the faction that currently
owns the airfield.
REQUIRES CIRIBOB's SIMPLE SLOT BLOCK (SSB) TO RUN ON THE SERVER
If run without SSB, your planes will not be blocked.
In order to work, a plane that should be blocked when the airfield or
FARP doesn't belong to the player's faction, the group's first unit
must be within 3000 meters of the airfield and on the ground.
Previous versions of this script relied on group names. No longer.
WARNING:
If you modified ssb's flag values, this script will not work
YOU DO NOT NEED TO ACTIVATE SBB, THIS SCRIPT DOES SO AUTOMAGICALLY
--]]--
-- below value for enabled MUST BE THE SAME AS THE VALUE OF THE SAME NAME
-- IN SSB. DEFAULT IS ZERO, AND THIS WILL WORK
cfxSSBClient.enabledFlagValue = 0 -- DO NOT CHANGE, MUST MATCH SSB
cfxSSBClient.disabledFlagValue = cfxSSBClient.enabledFlagValue + 100 -- DO NOT CHANGE
cfxSSBClient.allowNeutralFields = false -- set to FALSE if players can't spawn on neutral airfields
cfxSSBClient.maxAirfieldRange = 3000 -- meters to airfield before group is no longer associated with airfield
-- actions to home in on when a player plane is detected and a slot may
-- be blocked. Currently, homing in on airfield, but not fly over
cfxSSBClient.slotActions = {
"From Runway",
"From Parking Area",
"From Parking Area Hot",
"From Ground Area",
"From Ground Area Hot",
}
cfxSSBClient.keepInAirGroups = false -- if false we only look at planes starting on the ground
-- setting this to true only makes sense if you plan to bind in-air starts to airfields
cfxSSBClient.playerGroups = {}
cfxSSBClient.closedAirfields = {} -- list that closes airfields for any aircrafts
cfxSSBClient.playerPlanes = {} -- names of units that a player is flying
cfxSSBClient.crashedGroups = {} -- names of groups to block after crash of their player-flown plane
function cfxSSBClient.closeAirfieldNamed(name)
if not name then return end
cfxSSBClient.closedAirfields[name] = true
cfxSSBClient.setSlotAccessByAirfieldOwner()
if cfxSSBClient.verbose then
trigger.action.outText("+++SSB: Airfield " .. name .. " now closed", 30)
end
end
function cfxSSBClient.openAirFieldNamed(name)
cfxSSBClient.closedAirfields[name] = nil
cfxSSBClient.setSlotAccessByAirfieldOwner()
if cfxSSBClient.verbose then
trigger.action.outText("+++SSB: Airfield " .. name .. " just opened", 30)
end
end
function cfxSSBClient.unbindGroup(groupName)
cfxSSBClient.bindGroupToAirfield(groupName, nil)
end
function cfxSSBClient.bindGroupToAirfield(groupName, airfieldName)
if not groupName then return end
local airfield = nil
if airfieldName then airfield = Airbase.getByName(airfieldName) end
for idx, theGroup in pairs(cfxSSBClient.playerGroups) do
if theGroup.name == groupName then
if cfxSSBClient.verbose then
local newBind = "NIL"
if airfield then newBind = airfieldName end
trigger.action.outText("+++SSB: Group " .. theGroup.name .. " changed binding to " .. newBind, 30)
end
theGroup.airfield = airfield
return
end
end
if not airfieldName then airfieldName = "<NIL>" end
trigger.action.outText("+++SSB: Binding Group " .. groupName .. " to " .. airfieldName .. " failed.", 30)
end
--[[--
function cfxSSBClient.dist(point1, point2) -- returns distance between two points
local x = point1.x - point2.x
local y = point1.y - point2.y
local z = point1.z - point2.z
return (x*x + y*y + z*z)^0.5
end
--]]--
-- see if instring conatins what, defaults to case insensitive
--[[--
function cfxSSBClient.containsString(inString, what, caseSensitive)
if (not caseSensitive) then
inString = string.upper(inString)
what = string.upper(what)
end
return string.find(inString, what)
end
--]]--
--[[--
function cfxSSBClient.arrayContainsString(theArray, theString)
-- warning: case sensitive!
if not theArray then return false end
if not theString then return false end
for i = 1, #theArray do
if theArray[i] == theString then return true end
end
return false
end
--]]--
function cfxSSBClient.getClosestAirbaseTo(thePoint)
local delta = math.huge
local allYourBase = world.getAirbases() -- get em all
local closestBase = nil
for idx, aBase in pairs(allYourBase) do
-- iterate them all
local abPoint = aBase:getPoint()
newDelta = dcsCommon.dist(thePoint, {x=abPoint.x, y = 0, z=abPoint.z})
if newDelta < delta then
delta = newDelta
closestBase = aBase
end
end
return closestBase, delta
end
function cfxSSBClient.setSlotAccessForGroup(theGroup)
if not theGroup then return end
-- WARNING: theGroup is cfxGroup record
local theName = theGroup.name
local theMatchingAirfield = theGroup.airfield
-- airfield was attached at startup to group
if cfxSSBClient.singleUse and cfxSSBClient.crashedGroups[theName] then
-- we don't check, as we know it's blocked after crash
-- and leave it as it is. Nothing to do at all now
elseif theMatchingAirfield ~= nil then
-- we have found a plane that is tied to an airfield
-- so this group will receive a block/unblock
-- we always set all block/unblock every time
-- note: since caching, above guard not needed
local airFieldSide = theMatchingAirfield:getCoalition()
local groupCoalition = theGroup.coaNum
local blockState = cfxSSBClient.enabledFlagValue -- we default to ALLOW the block
local comment = "available"
-- see if airfield is closed
local afName = theMatchingAirfield:getName()
if cfxSSBClient.closedAirfields[afName] then
-- airfield is closed. no take-offs
blockState = cfxSSBClient.disabledFlagValue
comment = "!closed airfield!"
end
-- on top of that, check coalitions
if groupCoalition ~= airFieldSide then
-- we have a problem. sides don't match
if airFieldSide == 3
or (cfxSSBClient.allowNeutralFields and airFieldSide == 0)
then
-- all is well, airfield is contested or neutral and
-- we allow this plane to spawn here
else
-- DISALLOWED!!!!
blockState = cfxSSBClient.disabledFlagValue
comment = "!!!BLOCKED!!!"
end
end
-- set the ssb flag for this group so the server can see it
trigger.action.setUserFlag(theName, blockState)
if cfxSSBClient.verbose then
trigger.action.outText("+++SSB: group ".. theName .. ": " .. comment, 30)
end
else
if cfxSSBClient.verbose then
trigger.action.outText("+++SSB: group ".. theName .. " no bound airfield: available", 30)
end
end
end
function cfxSSBClient.getPlayerGroupForGroupNamed(aName)
local pGroups = cfxSSBClient.playerGroups
for idx, theGroup in pairs(pGroups) do
if theGroup.name == aName then return theGroup end
end
return nil
end
function cfxSSBClient.setSlotAccessByAirfieldOwner()
-- get all groups that have a player-controlled aircraft
-- now uses cached, reduced set of player planes
local pGroups = cfxSSBClient.playerGroups -- cfxGroups.getPlayerGroup() -- we want the group.name attribute
for idx, theGroup in pairs(pGroups) do
cfxSSBClient.setSlotAccessForGroup(theGroup)
end
end
function cfxSSBClient.reOpenSlotForGroupNamed(args)
-- this is merely the timer shell for opening the crashed slot
gName = args[1]
cfxSSBClient.openSlotForCrashedGroupNamed(gName)
end
function cfxSSBClient.openSlotForCrashedGroupNamed(gName)
if not gName then return end
local pGroup = cfxSSBClient.getPlayerGroupForGroupNamed(gName)
if not pGroup then return end
cfxSSBClient.crashedGroups[gName] = nil -- set to nil to forget this happened
cfxSSBClient.setSlotAccessForGroup(pGroup) -- set by current occupation status
trigger.action.outText("+++SSBC:SU: re-opened slot for group <" .. gName .. ">", 30)
end
function cfxSSBClient:onEvent(event)
if event.id == 10 then -- S_EVENT_BASE_CAPTURED
if cfxSSBClient.verbose then
trigger.action.outText("+++SSB: CAPTURE EVENT -- RESETTING SLOTS", 30)
end
cfxSSBClient.setSlotAccessByAirfieldOwner()
end
-- write down player names and planes
if event.id == 15 then
--trigger.action.outText("+++SSBC:SU: enter event 15", 30)
if not event.initiator then return end
local theUnit = event.initiator -- we know this exists
local uName = theUnit:getName()
if not uName then return end
-- player entered unit
local playerName = theUnit:getPlayerName()
if not playerName then
return -- NPC plane
end
-- remember this unit as player controlled plane
-- because player and plane can easily disconnect
cfxSSBClient.playerPlanes[uName] = playerName
trigger.action.outText("+++SSBC:SU: noted " .. playerName .. " piloting player unit " .. uName, 30)
return
end
if cfxSSBClient.singleUse and event.id == 5 then -- crash
if not event.initiator then return end
local theUnit = event.initiator
local uName = theUnit:getName()
if not uName then return end
local theGroup = theUnit:getGroup()
if not theGroup then return end
-- see if a player plane
local thePilot = cfxSSBClient.playerPlanes[uName]
if not thePilot then
-- ignore. not a player plane
trigger.action.outText("+++SSBC:SU: ignored crash for NPC unit <" .. uName .. ">", 30)
return
end
-- if we get here, a player-owned plane has crashed
local gName = theGroup:getName()
if not gName then return end
-- block this slot.
trigger.action.setUserFlag(gName, cfxSSBClient.disabledFlagValue)
-- remember this plane to not re-enable if
-- airfield changes hands later
cfxSSBClient.crashedGroups[gName] = thePilot -- set to crash pilot
trigger.action.outText("+++SSBC:SU: Blocked slot for group <" .. gName .. ">", 30)
if cfxSSBClient.reUseAfter > 0 then
-- schedule re-opening this slot in <x> seconds
timer.scheduleFunction(
cfxSSBClient.reOpenSlotForGroupNamed,
{gName},
timer.getTime() + cfxSSBClient.reUseAfter
)
end
end
end
function cfxSSBClient.update()
-- first, re-schedule me in one minute
timer.scheduleFunction(cfxSSBClient.update, {}, timer.getTime() + 60)
-- now establish all slot blocks
cfxSSBClient.setSlotAccessByAirfieldOwner()
end
-- pre-process static player data to minimize
-- processor load on checks
function cfxSSBClient.processPlayerData()
cfxSSBClient.playerGroups = cfxGroups.getPlayerGroup()
local pGroups = cfxSSBClient.playerGroups
local filteredPlayers = {}
for idx, theGroup in pairs(pGroups) do
if theGroup.airfield ~= nil or cfxSSBClient.keepInAirGroups or
cfxSSBClient.singleUse then
-- only transfer groups that have airfields (or also keepInAirGroups or when single-use)
-- attached. Ignore the rest as they are
-- always fine
table.insert(filteredPlayers, theGroup)
end
end
cfxSSBClient.playerGroups = filteredPlayers
end
-- add airfield information to each player group
function cfxSSBClient.processGroupData()
local pGroups = cfxGroups.getPlayerGroup() -- we want the group.name attribute
for idx, theGroup in pairs(pGroups) do
-- we always use the first player's plane as referenced
local playerData = theGroup.playerUnits[1]
local theAirfield = nil
local delta = -1
local action = playerData.action
if not action then action = "<NIL>" end
-- see if the data has any of the slot-interesting actions
if dcsCommon.arrayContainsString(cfxSSBClient.slotActions, action ) then
-- yes, fetch the closest airfield
theAirfield, delta = cfxSSBClient.getClosestAirbaseTo(playerData.point)
local afName = theAirfield:getName()
if cfxSSBClient.verbose then
trigger.action.outText("+++SSB: group: " .. theGroup.name .. " closest to AF " .. afName .. ": " .. delta .. "m" , 30)
end
if delta > cfxSSBClient.maxAirfieldRange then
-- forget airfield
theAirfield = nil
if cfxSSBClient.verbose then
trigger.action.outText("+++SSB: group: " .. theGroup.name .. " unlinked - too far from airfield" , 30)
end
end
theGroup.airfield = theAirfield
else
if cfxSSBClient.verbose then
trigger.action.outText("+++SSB: group: " .. theGroup.name .. " start option " .. action .. " does not concern SSB", 30)
end
end
end
end
--
-- read config zone
--
function cfxSSBClient.readConfigZone()
-- note: must match exactly!!!!
local theZone = cfxZones.getZoneByName("SSBClientConfig")
if not theZone then
trigger.action.outText("+++SSBC: no config zone!", 30)
return
end
trigger.action.outText("+++SSBC: found config zone!", 30)
cfxSSBClient.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
-- single-use
cfxSSBClient.singleUse = cfxZones.getBoolFromZoneProperty(theZone, "singleUse", false) -- use airframes only once? respawn after kick must be disabled in ssb
cfxSSBClient.reUseAfter = cfxZones.getNumberFromZoneProperty(theZone, "reUseAfter", -1)
-- airfield availability
cfxSSBClient.allowNeutralFields = cfxZones.getBoolFromZoneProperty(theZone, "allowNeutralFields", false)
cfxSSBClient.maxAirfieldRange = cfxZones.getNumberFromZoneProperty(theZone, "maxAirfieldRange", 3000) -- meters, to find attached airfield
-- optimization
cfxSSBClient.keepInAirGroups = cfxZones.getBoolFromZoneProperty(theZone, "keepInAirGroups", false)
-- SSB direct control.
-- USE ONLY WHEN YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
cfxSSBClient.enabledFlagValue = cfxZones.getNumberFromZoneProperty(theZone, "enabledFlagValue", 0)
cfxSSBClient.disabledFlagValue = cfxZones.getNumberFromZoneProperty(theZone, "disabledFlagValue", cfxSSBClient.enabledFlagValue + 100)
end
--
-- start
--
function cfxSSBClient.start()
-- verify modules loaded
if not dcsCommon.libCheck("cfx SSB Client",
cfxSSBClient.requiredLibs) then
return false
end
-- read config zone if present
cfxSSBClient.readConfigZone()
-- install callback for events in DCS
world.addEventHandler(cfxSSBClient)
-- process group data to attach airfields
cfxSSBClient.processGroupData()
-- process player data to minimize effort and build cache
-- into cfxSSBClient.playerGroups
cfxSSBClient.processPlayerData()
-- install a timed update just to make sure
-- and start NOW
timer.scheduleFunction(cfxSSBClient.update, {}, timer.getTime() + 1)
-- now turn on ssb
trigger.action.setUserFlag("SSB",100)
-- say hi!
trigger.action.outText("cfxSSBClient v".. cfxSSBClient.version .. " running, SBB enabled", 30)
--cfxSSBClient.allYourBase()
return true
end
if not cfxSSBClient.start() then
trigger.action.outText("cfxSSBClient v".. cfxSSBClient.version .. " FAILED loading.", 30)
cfxSSBClient = nil
end
--[[--
possible improvements:
- use explicitBlockList that with API. planes on that list are always blocked. Use this for special effects, such as allowing a slot only to open from scripts, e.g. when a condition is met like money or goals reached
-]]--

154
modules/cfxSSBSingleUse.lua Normal file
View File

@ -0,0 +1,154 @@
cfxSSBSingleUse = {}
cfxSSBSingleUse.version = "1.1.0"
--[[--
Version History
1.0.0 - Initial version
1.1.0 - importing dcsCommon, cfxGroup for simplicity
- save unit name on player enter unit as look-up
- determining ground-start
- place wreck in slot
1.1.1 - guarding against nil playerName
- using 15 (birth) instead of 20 (player enter)
WHAT IT IS
SSB Single Use is a script that blocks a player slot
after that plane crashes.
--]]--
cfxSSBSingleUse.enabledFlagValue = 0 -- DO NOT CHANGE, MUST MATCH SSB
cfxSSBSingleUse.disabledFlagValue = cfxSSBSingleUse.enabledFlagValue + 100 -- DO NOT CHANGE
cfxSSBSingleUse.playerUnits = {}
cfxSSBSingleUse.slotGroundActions = {
-- "From Runway", -- NOT RUNWAY, as that would litter runway
"From Parking Area",
"From Parking Area Hot",
"From Ground Area",
"From Ground Area Hot",
}
cfxSSBSingleUse.groundSlots = {} -- players that start on the ground
function cfxSSBSingleUse:onEvent(event)
if not event then return end
if not event.id then return end
if not event.initiator then return end
-- if we get here, initiator is set
local theUnit = event.initiator -- we know this exists
-- write down player names and planes
if event.id == 15 then
local uName = theUnit:getName()
if not uName then return end
-- player entered unit
local playerName = theUnit:getPlayerName()
if not playerName then
return -- NPC plane
end
-- remember this unit as player unit
cfxSSBSingleUse.playerUnits[uName] = playerName
trigger.action.outText("+++singleUse: noted " .. playerName .. " piloting player unit " .. uName, 30)
return
end
-- check for a crash
if event.id == 5 then -- S_EVENT_CRASH
local uName = theUnit:getName()
if not uName then return end
local theGroup = theUnit:getGroup()
if not theGroup then return end
-- see if a player plane
local thePilot = cfxSSBSingleUse.playerUnits[uName]
if not thePilot then
-- ignore. not a player plane
trigger.action.outText("+++singleUse: ignored crash for NPC unit <" .. uName .. ">", 30)
return
end
local gName = theGroup:getName()
if not gName then return end
-- see if it was a ground slot
local theGroundSlot = cfxSSBSingleUse.groundSlots[gName]
if theGroundSlot then
local unitType = theUnit:getTypeName()
trigger.action.outText("+++singleUse: <" .. uName .. "> starts on Ground. Will place debris for " .. unitType .. " NOW!!!", 30)
cfxSSBSingleUse.placeDebris(unitType, theGroundSlot)
end
-- block this slot.
trigger.action.setUserFlag(gName, cfxSSBSingleUse.disabledFlagValue)
trigger.action.outText("+++singleUse: blocked <" .. gName .. "> after " .. thePilot .. " crashed it.", 30)
end
end
function cfxSSBSingleUse.placeDebris(unitType, theGroundSlot)
if not unitType then return end
-- access location one, we assume single-unit groups
-- or at least that the player sits in unit one
local playerData = theGroundSlot.playerUnits
local theSlotData = playerData[1]
local wreckData = {}
wreckData.heading = 0
wreckData.name = dcsCommon.uuid("singleUseWreck"..theSlotData.name)
wreckData.x = tonumber(theSlotData.point.x)
wreckData.y = tonumber(theSlotData.point.z)
wreckData.dead = true
wreckData.type = unitType
coalition.addStaticObject(theGroundSlot.coaNum, wreckData )
trigger.action.outText("+++singleUse: wreck <" .. unitType .. "> at " .. wreckData.x .. ", " .. wreckData.y .. " for " .. wreckData.name, 30)
end
function cfxSSBSingleUse.populateAirfieldSlots()
local pGroups = cfxGroups.getPlayerGroup()
local groundStarters = {}
for idx, theGroup in pairs(pGroups) do
-- we always use the first player's plane as referenced
local playerData = theGroup.playerUnits[1]
local action = playerData.action
if not action then action = "<NIL>" end
-- see if the data has any of the slot-interesting actions
if dcsCommon.arrayContainsString(cfxSSBSingleUse.slotGroundActions, action ) then
-- ground starter, not from runway
groundStarters[theGroup.name] = theGroup
trigger.action.outText("+++singleUse: <" .. theGroup.name .. "> is ground starter", 30)
end
end
cfxSSBSingleUse.groundSlots = groundStarters
end
function cfxSSBSingleUse.start()
-- install event monitor
world.addEventHandler(cfxSSBSingleUse)
-- get all groups and process them to find
-- all planes that are on the ground for
-- eye candy
cfxSSBSingleUse.populateAirfieldSlots()
-- turn on ssb
trigger.action.setUserFlag("SSB",100)
trigger.action.outText("SSB Single use v" .. cfxSSBSingleUse.version .. " running", 30)
end
-- let's go!
cfxSSBSingleUse.start()
--[[--
Additional features (later):
- place a wreck in slot when blocking for eye candy
- record player when they enter a unit and only block player planes
--]]--

111
modules/cfxSmokeZones.lua Normal file
View File

@ -0,0 +1,111 @@
cfxSmokeZone = {}
cfxSmokeZone.version = "1.0.2"
cfxSmokeZone.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
}
--[[--
Version History
1.0.0 - initial version
1.0.1 - added removeSmokeZone
1.0.2 - added altitude
SMOKE ZONES *** EXTENDS ZONES ***
keeps 'eternal' smoke up for any zone that has the
'smoke' attribute
USAGE
add a 'smoke' attribute to the zone. the value of the attribute
defines the color. Valid values are: red, green, blue, white, orange, 0 (results in green smoke), 1 (red smoke), 2 (white), 3 (orange), 4 (blue)
defaults to "green"
altiude is meters above ground height, defaults to 5m
--]]--
cfxSmokeZone.smokeZones = {}
cfxSmokeZone.updateDelay = 5 * 60 -- every 5 minutes
function cfxSmokeZone.processSmokeZone(aZone)
local rawVal = cfxZones.getStringFromZoneProperty(aZone, "smoke", "green")
rawVal = rawVal:lower()
local theColor = 0
if rawVal == "red" or rawVal == "1" then theColor = 1 end
if rawVal == "white" or rawVal == "2" then theColor = 2 end
if rawVal == "orange" or rawVal == "3" then theColor = 3 end
if rawVal == "blue" or rawVal == "4" then theColor = 4 end
aZone.smokeColor = theColor
aZone.smokeAlt = cfxZones.getNumberFromZoneProperty(aZone, "altitude", 1)
end
function cfxSmokeZone.addSmokeZone(aZone)
table.insert(cfxSmokeZone.smokeZones, aZone)
end
function cfxSmokeZone.addSmokeZoneWithColor(aZone, aColor, anAltitude)
if not aColor then aColor = 0 end -- default green
if not anAltitude then anAltitude = 5 end
if not aZone then return end
aZone.smokeColor = aColor
aZone.smokeAlt = anAltitude
cfxSmokeZone.addSmokeZone(aZone) -- add to update loop
cfxZones.markZoneWithSmoke(aZone, 0, 0, aZone.smokeColor, aZone.smokeAlt) -- smoke on!
end
function cfxSmokeZone.removeSmokeZone(aZone)
if not aZone then return end
if type(aZone) == "string" then
aZone = cfxZones.getZoneByName(aZone)
end
-- now create new table
local filtered = {}
for idx, theZone in pairs(cfxSmokeZone.smokeZones) do
if theZone ~= aZone then
table.insert(filtered, theZone)
end
end
cfxSmokeZone.smokeZones = filtered
end
function cfxSmokeZone.update()
-- call me in a couple of minutes to 'rekindle'
timer.scheduleFunction(cfxSmokeZone.update, {}, timer.getTime() + cfxSmokeZone.updateDelay)
-- re-smoke all zones after delay
for idx, aZone in pairs(cfxSmokeZone.smokeZones) do
if aZone.smokeColor then
cfxZones.markZoneWithSmoke(aZone, 0, 0, aZone.smokeColor, aZone.smokeAlt)
end
end
end
function cfxSmokeZone.start()
if not dcsCommon.libCheck("cfx Smoke Zones",
cfxSmokeZone.requiredLibs) then
return false
end
-- collect all zones with 'smoke' attribute
-- collect all spawn zones
local attrZones = cfxZones.getZonesWithAttributeNamed("smoke")
-- now create a spawner for all, add them to the spawner updater, and spawn for all zones that are not
-- paused
for k, aZone in pairs(attrZones) do
cfxSmokeZone.processSmokeZone(aZone) -- process attribute and add to zone
cfxSmokeZone.addSmokeZone(aZone) -- remember it so we can smoke it
end
-- start update loop
cfxSmokeZone.update()
-- say hi
trigger.action.outText("cfx Smoke Zones v" .. cfxSmokeZone.version .. " started.", 30)
return true
end
-- let's go
if not cfxSmokeZone.start() then
trigger.action.outText("cf/x Smoke Zones aborted: missing libraries", 30)
cfxSmokeZone = nil
end

485
modules/cfxSpawnZones.lua Normal file
View File

@ -0,0 +1,485 @@
cfxSpawnZones = {}
cfxSpawnZones.version = "1.5.1"
cfxSpawnZones.requiredLibs = {
"dcsCommon", -- common is of course needed for everything
-- pretty stupid to check for this since we
-- need common to invoke the check, but anyway
"cfxZones", -- Zones, of course
"cfxCommander", -- to make troops do stuff
"cfxGroundTroops", -- generic data module for weight
}
cfxSpawnZones.ups = 1
cfxSpawnZones.verbose = false
--
-- 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
--
-- 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
cfxSpawnZones.allSpawners = {}
cfxSpawnZones.callbacks = {} -- signature: cb(reason, group, spawner)
--
-- C A L L B A C K S
--
function cfxSpawnZones.addCallback(theCallback)
table.insert(cfxSpawnZones.callbacks, theCallback)
end
function cfxSpawnZones.invokeCallbacksFor(reason, theGroup, theSpawner)
for idx, theCB in pairs (cfxSpawnZones.callbacks) do
theCB(reason, theGroup, theSpawner)
end
end
--
-- creating a spawner
--
function cfxSpawnZones.createSpawner(inZone)
local theSpawner = {}
theSpawner.zone = inZone
theSpawner.name = inZone.name
-- connect with ME if a trigger flag is given
if cfxZones.hasProperty(inZone, "f?") then
theSpawner.triggerFlag = cfxZones.getStringFromZoneProperty(inZone, "f?", "none")
theSpawner.lastTriggerValue = trigger.misc.getUserFlag(theSpawner.triggerFlag)
end
theSpawner.types = cfxZones.getZoneProperty(inZone, "types")
--theSpawner.owner = cfxZones.getCoalitionFromZoneProperty(inZone, "owner", 0)
-- synthesize types * typeMult
local n = cfxZones.getNumberFromZoneProperty(inZone, "typeMult", 1)
local repeater = ""
if n < 1 then n = 1 end
while n > 1 do
repeater = repeater .. "," .. theSpawner.types
n = n - 1
end
theSpawner.types = theSpawner.types .. repeater
theSpawner.country = cfxZones.getNumberFromZoneProperty(inZone, "country", 0) -- coalition2county(theSpawner.owner)
theSpawner.masterZoneName = cfxZones.getStringFromZoneProperty(inZone, "masterOwner", "")
if theSpawner.masterZoneName == "" then theSpawner.masterZoneName = nil end
theSpawner.rawOwner = coalition.getCountryCoalition(theSpawner.country)
--theSpawner.baseName = cfxZones.getZoneProperty(inZone, "baseName")
theSpawner.baseName = cfxZones.getStringFromZoneProperty(inZone, "baseName", dcsCommon.uuid("SpwnDflt"))
theSpawner.cooldown = cfxZones.getNumberFromZoneProperty(inZone, "cooldown", 60)
theSpawner.autoRemove = cfxZones.getBoolFromZoneProperty(inZone, "autoRemove", false)
theSpawner.lastSpawnTimeStamp = -10000 -- just init so it will always work
theSpawner.heading = cfxZones.getNumberFromZoneProperty(inZone, "heading", 0)
--trigger.action.outText("+++spwn: zone " .. inZone.name .. " owner " .. theSpawner.owner " --> ctry " .. theSpawner.country, 30)
theSpawner.cdTimer = 0 -- used for cooldown. if timer.getTime < this value, don't spawn
theSpawner.cdStarted = false -- used to initiate cooldown when theSpawn disappears
theSpawner.count = 1 -- used to create names, and count how many groups created
theSpawner.theSpawn = nil -- link to last spawned group
theSpawner.formation = "circle_out"
theSpawner.formation = cfxZones.getStringFromZoneProperty(inZone, "formation", "circle_out")
theSpawner.paused = cfxZones.getBoolFromZoneProperty(inZone, "paused", false)
theSpawner.orders = cfxZones.getStringFromZoneProperty(inZone, "orders", "guard")
--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.
-- 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
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
return theSpawner
end
function cfxSpawnZones.addSpawner(aSpawner)
cfxSpawnZones.allSpawners[aSpawner.zone] = aSpawner
end
function cfxSpawnZones.removeSpawner(aSpawner)
cfxSpawnZones.allSpawners[aSpawner.zone] = nil
end
function cfxSpawnZones.getSpawnerForZone(aZone)
return cfxSpawnZones.allSpawners[aZone]
end
function cfxSpawnZones.getSpawnerForZoneNamed(aName)
local aZone = cfxZones.getZoneByName(aName)
return cfxSpawnZones.getSpawnerForZone(aZone)
end
function cfxSpawnZones.getRequestableSpawnersInRange(aPoint, aRange, aSide)
-- trigger.action.outText("enter requestable spawners for side " .. aSide , 30)
if not aSide then aSide = 0 end
if not aRange then aRange = 200 end
if not aPoint then return {} end
local theSpawners = {}
for aZone, aSpawner in pairs(cfxSpawnZones.allSpawners) do
-- iterate all zones and collect those that match
local hasMatch = true
local delta = dcsCommon.dist(aPoint, aZone.point)
if delta>aRange then hasMatch = false end
if aSide ~= 0 then
-- check if side is correct for owned zone
if not cfxSpawnZones.verifySpawnOwnership(aSpawner) then
-- failed ownership test. owner of master
-- is not my own zone
hasMatch = false
end
end
if aSide ~= aSpawner.rawOwner then
-- only return spawners with this side
-- note: this will NOT work with neutral players
hasMatch = false
end
if not aSpawner.requestable then
hasMatch = false
end
if hasMatch then
table.insert(theSpawners, aSpawner)
end
end
return theSpawners
end
--
-- spawn troops
--
function cfxSpawnZones.verifySpawnOwnership(spawner)
-- returns false ONLY if masterSpawn disagrees
if not spawner.masterZoneName then
--trigger.action.outText("spawner " .. spawner.name .. " no master, go!", 30)
return true
end -- no master owner, all ok
local myCoalition = spawner.rawOwner
local masterZone = cfxZones.getZoneByName(spawner.masterZoneName)
if not masterZone then
trigger.action.outText("spawner " .. spawner.name .. " DID NOT FIND MASTER ZONE <" .. spawner.masterZoneName .. ">", 30)
end
if not masterZone.owner then
--trigger.action.outText("spawner " .. spawner.name .. " - masterZone " .. masterZone.name .. " HAS NO OWNER????", 30)
return true
end
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
function cfxSpawnZones.spawnWithSpawner(aSpawner)
if type(aSpawner) == "string" then -- return spawner for zone of that name
aSpawner = cfxSpawnZones.getSpawnerForZoneNamed(aName)
end
if not aSpawner then return end
-- will NOT check if conditions are met. This forces a spawn
local unitTypes = {} -- build type names
--local p = aSpawner.zone.point
local p = cfxZones.getPoint(aSpawner.zone) -- aSpawner.zone.point
-- split the conf.troopsOnBoardTypes into an array of types
unitTypes = dcsCommon.splitString(aSpawner.types, ",")
if #unitTypes < 1 then
table.insert(unitTypes, "Soldier M4") -- make it one m4 trooper as fallback
end
local theCountry = aSpawner.country
local theCoalition = coalition.getCountryCoalition(theCountry)
-- trigger.action.outText("+++ spawn: coal <" .. theCoalition .. "> from country <" .. theCountry .. ">", 30)
local theGroup = cfxZones.createGroundUnitsInZoneForCoalition (
theCoalition,
aSpawner.baseName .. "-" .. aSpawner.count, -- must be unique
aSpawner.zone,
unitTypes,
aSpawner.formation,
aSpawner.heading)
aSpawner.theSpawn = theGroup
aSpawner.count = aSpawner.count + 1
-- we may also want to add this to auto ground troops pool
-- we not only want to, we absolutely need to in order
-- to make this work.
if aSpawner.orders and (
aSpawner.orders:lower() == "training" or
aSpawner.orders:lower() == "train" )
then
-- make them ROE "HOLD"
cfxCommander.scheduleOptionForGroup(
theGroup,
AI.Option.Ground.id.ROE,
AI.Option.Ground.val.ROE.WEAPON_HOLD,
1.0)
else
local newTroops = cfxGroundTroops.createGroundTroops(theGroup, aSpawner.range, aSpawner.orders)
cfxGroundTroops.addGroundTroopsToPool(newTroops)
-- see if we have defined a target zone as destination
if aSpawner.target then
local destZone = cfxZones.getZoneByName(aSpawner.target)
if destZone then
newTroops.destination = destZone
cfxGroundTroops.makeTroopsEngageZone(newTroops)
else
trigger.action.outText("+++ spawner " .. aSpawner.name .. " has illegal target " .. aSpawner.target .. ". Pausing.", 30)
aSpawner.paused = true
end
elseif aSpawner.orders == "attackZone" then
trigger.action.outText("+++ spawner " .. aSpawner.name .. " has no target but attackZone command. Pausing.", 30)
aSpawner.paused = true
end
end
-- callback to all who want to know
cfxSpawnZones.invokeCallbacksFor("spawned", theGroup, aSpawner)
-- timestamp so we can check against cooldown on manual spawn
aSpawner.lastSpawnTimeStamp = timer.getTime()
-- make sure a requestable spawner is always paused
if aSpawner.requestable then
aSpawner.paused = true
end
if aSpawner.autoRemove then
-- simply remove the group
aSpawner.theSpawn = nil
end
end
--
-- U P D A T E
--
function cfxSpawnZones.update()
cfxSpawnZones.updateSchedule = timer.scheduleFunction(cfxSpawnZones.update, {}, timer.getTime() + 1/cfxSpawnZones.ups)
for key, spawner in pairs (cfxSpawnZones.allSpawners) do
-- see if the spawn is dead or was removed
local needsSpawn = true
if spawner.theSpawn then
local group = spawner.theSpawn
if group:isExist() then
-- see how many members of this group are still alive
local liveUnits = dcsCommon.getLiveGroupUnits(group)
-- spawn is still alive, will not spawn
if #liveUnits > 1 then
-- we may want to check if this member is still inside
-- of spawn location. currently we don't do that
needsSpawn = false
end
end
end
if spawner.paused then needsSpawn = false end
-- see if we spawned maximum number of times already
-- or have -1 as maxspawn, indicating endless
if needsSpawn and spawner.maxSpawns > -1 then
needsSpawn = spawner.maxSpawns > 0
end
if needsSpawn then
-- is this the first time?
if not spawner.cdStarted then
-- no, start cooldown
spawner.cdStarted = true
spawner.cdTimer = timer.getTime() + spawner.cooldown
end
end
-- still on cooldown?
if timer.getTime() < spawner.cdTimer then needsSpawn = false end
-- is master zone still alinged with me?
needsSpawn = needsSpawn and cfxSpawnZones.verifySpawnOwnership(spawner)
-- check if perhaps our watchtrigger causes spawn
if spawner.triggerFlag then
local currTriggerVal = trigger.misc.getUserFlag(spawner.triggerFlag)
if currTriggerVal ~= spawner.lastTriggerValue then
needsSpawn = true
spawner.lastTriggerValue = currTriggerVal
end
end
-- if we get here, and needsSpawn is still set, we go ahead and spawn
if needsSpawn then
--- trigger.action.outText("+++ spawning for zone " .. spawner.zone.name, 30)
cfxSpawnZones.spawnWithSpawner(spawner)
spawner.cdStarted = false -- reset spawner cd signal
if spawner.maxSpawns > 0 then
spawner.maxSpawns = spawner.maxSpawns - 1
end
if spawner.maxSpawns == 0 then
spawner.paused = true
if cfxSpawnZones.verbose then
trigger.action.outText("+++ maxspawn -- turning off zone " .. spawner.zone.name, 30)
end
end
else
-- trigger.action.outText("+++ NOSPAWN for zone " .. spawner.zone.name, 30)
end
end
end
function cfxSpawnZones.start()
if not dcsCommon.libCheck("cfx Spawn Zones",
cfxSpawnZones.requiredLibs) then
return false
end
-- collect all spawn zones
local attrZones = cfxZones.getZonesWithAttributeNamed("spawner")
-- now create a spawner for all, add them to the spawner updater, and spawn for all zones that are not
-- paused
for k, aZone in pairs(attrZones) do
local aSpawner = cfxSpawnZones.createSpawner(aZone)
cfxSpawnZones.addSpawner(aSpawner)
if not aSpawner.paused and cfxSpawnZones.verifySpawnOwnership(aSpawner) and aSpawner.maxSpawns ~= 0 then
cfxSpawnZones.spawnWithSpawner(aSpawner)
-- update spawn count and make sure we haven't spawned the one and only
if aSpawner.maxSpawns > 0 then
aSpawner.maxSpawns = aSpawner.maxSpawns - 1
end
if aSpawner.maxSpawns == 0 then
aSpawner.paused = true
trigger.action.outText("+++ maxspawn -- turning off zone " .. aSpawner.zone.name, 30)
end
end
end
-- and start the regular update calls
cfxSpawnZones.update()
trigger.action.outText("cfx Spawn Zones v" .. cfxSpawnZones.version .. " started.", 30)
return true
end
if not cfxSpawnZones.start() then
trigger.action.outText("cf/x Spawn Zones aborted: missing libraries", 30)
cfxSpawnZones = nil
end
--[[--
IMPROVEMENTS
'notMasterOwner' a flag to invert ownership, so we can spawn blue if masterOwner is red
take apart owned zone and spawner, so we have a more canonical behaviour
'repair' flag - have repair logic for units that are spawned just like now the owned zones do
--]]--

1323
modules/cfxZones.lua Normal file

File diff suppressed because it is too large Load Diff

133
modules/cfxmon.lua Normal file
View File

@ -0,0 +1,133 @@
cfxmon = {}
cfxmon.version = "1.0.0"
cfxmon.delay = 30 -- seconds for display
--[[--
Version History
1.0.0 - initial version
cfxmon is a monitor for all cfx events and callbacks
use monConfig to tell cfxmon which events and callbacks
to monitor. a Property with "no" or "false" will turn
that monitor OFF, else it will stay on
supported modules if loaded
dcsCommon
cfxPlayer
cfxGroundTroops
cfxObjectDestructDetector
cfxSpawnZones
--]]--
--
-- CALLBACKS
--
-- dcsCommon Callbacks
function cfxmon.pre(event)
trigger.action.outText("***mon - dcsPre: " .. event.id .. " (" .. dcsCommon.event2text(event.id) .. ")", cfxmon.delay)
return true
end
function cfxmon.post(event)
trigger.action.outText("***mon - dcsPost: " .. event.id .. " (" .. dcsCommon.event2text(event.id) .. ")", cfxmon.delay)
end
function cfxmon.rejected(event)
trigger.action.outText("***mon - dcsReject: " .. event.id .. " (" .. dcsCommon.event2text(event.id) .. ")", cfxmon.delay)
end
function cfxmon.dcsCB(event)
local initiatorStat = ""
if event.initiator then
local theUnit = event.initiator
local theGroup = theUnit:getGroup()
local theGroupName = "<none>"
if theGroup then theGroupName = theGroup:getName() end
initiatorStat = ", for " .. theUnit:getName()
initiatorStat = initiatorStat .. " of " .. theGroupName
else
initiatorStat = ", NO Initiator"
end
trigger.action.outText("***mon - dcsMAIN: " .. event.id .. " (" .. dcsCommon.event2text(event.id) .. ")" .. initiatorStat, cfxmon.delay)
end
-- cfxPlayer callback
function cfxmon.playerEventCB(evType, description, info, data)
trigger.action.outText("***mon - cfxPlayer: ".. evType ..": <" .. description .. ">", cfxmon.delay)
end
-- cfxGroundTroops callback
function cfxmon.groundTroopsCB(reason, theGroup, orders, data)
trigger.action.outText("***mon - groundTroops: ".. reason ..": for group <" .. theGroup:getName() .. "> with orders " .. orders, cfxmon.delay)
end
-- object destruct callbacks
function cfxmon.oDestructCB(zone, ObjectID, name)
trigger.action.outText("***mon - object destroyed: ".. ObjectID .." named <" .. name .. "> in zone " .. zone.name, cfxmon.delay)
end
-- spawner callback
function cfxmon.spawnZoneCB(reason, theGroup, theSpawner)
local gName = "<nil>"
if theGroup then gName = theGroup:getName() end
trigger.action.outText("***mon - Spawner: ".. reason .." group <" .. gName .. "> in zone " .. theSpawner.name, cfxmon.delay)
end
-- READ CONFIG AND SUBSCRIBE
function cfxmon.start ()
local theZone = cfxZones.getZoneByName("monConfig")
if not theZone then
trigger.action.outText("***mon: WARNING: NO config, defaulting", cfxmon.delay)
theZone = cfxZones.createSimpleZone("MONCONFIG")
end
-- own config
cfxmon.delay = cfxZones.getNumberFromZoneProperty(theZone, "delay", 30)
trigger.action.outText("!!!mon: Delay is set to: " .. cfxmon.delay .. seconds, 50)
-- dcsCommon
if cfxZones.getBoolFromZoneProperty(theZone, "dcsCommon", true) then
-- subscribe to dcs event handlers
-- note we have all, but only connect the main
dcsCommon.addEventHandler(cfxmon.dcsCB) -- we only connect one
trigger.action.outText("!!!mon: +dcsCommon", cfxmon.delay)
else
trigger.action.outText("***mon: -dcsCommon", cfxmon.delay)
end
-- cfxPlayer
if cfxPlayer and cfxZones.getBoolFromZoneProperty(theZone, "cfxPlayer", true) then
cfxPlayer.addMonitor(cfxmon.playerEventCB)
trigger.action.outText("!!!mon: +cfxPlayer", cfxmon.delay)
else
trigger.action.outText("***mon: -cfxPlayer", cfxmon.delay)
end
-- cfxGroundTroops
if cfxGroundTroops and cfxZones.getBoolFromZoneProperty(theZone, "cfxGroundTroops", true) then
cfxGroundTroops.addTroopsCallback(cfxmon.groundTroopsCB)
trigger.action.outText("!!!mon: +cfxGroundTroops", cfxmon.delay)
else
trigger.action.outText("***mon: -cfxGroundTroops", cfxmon.delay)
end
-- objectDestructZones
if cfxObjectDestructDetector and cfxZones.getBoolFromZoneProperty(theZone, "cfxObjectDestructDetector", true) then
cfxObjectDestructDetector.addCallback(cfxmon.oDestructCB)
trigger.action.outText("!!!mon: +cfxObjectDestructDetector", cfxmon.delay)
else
trigger.action.outText("***mon: -cfxObjectDestructDetector", cfxmon.delay)
end
-- spawnZones
if cfxSpawnZones and cfxZones.getBoolFromZoneProperty(theZone, "cfxSpawnZones", true) then
cfxSpawnZones.addCallback(cfxmon.spawnZoneCB)
trigger.action.outText("!!!mon: +cfxSpawnZones", cfxmon.delay)
else
trigger.action.outText("***mon: -cfxSpawnZones", cfxmon.delay)
end
end
cfxmon.start()

572
modules/civAir.lua Normal file
View File

@ -0,0 +1,572 @@
civAir = {}
civAir.version = "1.4.0"
--[[--
1.0.0 initial version
1.1.0 exclude list for airfields
1.1.1 bug fixes with remove flight
and betweenHubs
check if slot is really free before spawning
add overhead waypoint
1.1.2 inAir start possible
1.2.0 civAir can use own config file
1.2.1 slight update to config file (moved active/idle)
1.3.0 add ability to use zones to add closest airfield to
trafficCenters or excludeAirfields
1.4.0 ability to load config from zone to override
all configs it finds
module check
removed obsolete civAirConfig module
--]]--
civAir.ups = 0.05 -- updates per second
civAir.initialAirSpawns = true -- when true has population spawn in-air at start
civAir.verbose = false
-- aircraftTypes contains the type names for the neutral air traffic
-- each entry has the same chance to be chose, so to make an
-- aircraft more probably to appear, add its type multiple times
-- like here with the Yak-40
civAir.aircraftTypes = {"Yak-40", "Yak-40", "C-130", "C-17A", "IL-76MD", "An-30M", "An-26B"} -- civilian planes type strings as described here https://github.com/mrSkortch/DCS-miscScripts/tree/master/ObjectDB
-- maxTraffic is the number of neutral flights that are
-- concurrently under way
civAir.maxTraffic = 10 -- number of flights at the same time
civAir.maxIdle = 8 * 60 -- seconds of ide time before it is removed after landing
civAir.trafficAirbases = {
randomized = 0, -- between any on map
localHubs = 1, -- between any two airfields inside the same random hub listed in trafficCenters
betweenHubs = 2 -- between any in random hub 1 to any in random hub 2
}
civAir.trafficRange = 100 -- 120000 -- defines hub size, in meters. Make it 100 to make it only that airfield
-- ABPickmethod determines how airfields are picked
-- for air traffic
civAir.ABPickMethod = civAir.trafficAirbases.betweenHubs
civAir.trafficCenters = {
--"batu",
--"kobul",
--"senaki",
--"kutai",
} -- trafficCenters is used with hubs. Each entry defines a hub
-- where we collect airdromes etc based on range
-- simply add a string to identify the hub center
-- e.g. "senak" to define "Senaki Kolkhi"
-- to have planes only fly between airfields in 100 km range
-- around senaki kolkhi, enter only senaki as traffic center, set
-- trafficRange to 100000 and ABPickMethod to localHubs
-- to have traffic only between any airfields listed
-- in trafficCenters, set trafficRange to a small value
-- like 100 meters and set ABPickMethod to betweenHubs
-- to have flights that always cross the map with multiple
-- airfields, choose two or three hubs that are 300 km apart,
-- then set trafficRange to 150000 and ABPickMethod to betweenHubs
-- you can also place zones on the map and add a
-- civAir attribute. If the attribute value is anything
-- but "exclude", the closest airfield to the zone
-- is added to trafficCenters
-- if you leave this list empty, and do not add airfields
-- by zones, the list is automatically populated by all
-- airfields in the map
civAir.excludeAirfields = {
--"senaki",
}
-- list all airfields that must NOT be included in
-- civilian activities. Will be used for neither landing
-- nor departure. overrides any airfield that was included
-- in trafficCenters. Here, Senaki is off limits for
-- civilian air traffic
-- can be populated by zone on the map that have the
-- 'civAir' attribute with value "exclude"
civAir.requiredLibs = {
"dcsCommon", -- common is of course needed for everything
"cfxZones", -- zones management foc CSAR and CSAR Mission zones
}
civAir.activePlanes = {}
civAir.idlePlanes = {}
function civAir.readConfigZone()
-- note: must match exactly!!!!
local theZone = cfxZones.getZoneByName("CivAirConfig")
if not theZone then
trigger.action.outText("***civA: NO config zone!", 30)
return
end
trigger.action.outText("civA: found config zone!", 30)
-- ok, for each property, load it if it exists
if cfxZones.hasProperty(theZone, "aircraftTypes") then
civAir.aircraftTypes = cfxZones.getStringFromZoneProperty(theZone, "aircraftTypes", "Yak-40")
end
if cfxZones.hasProperty(theZone, "ups") then
civAir.ups = cfxZones.getNumberFromZoneProperty(theZone, "ups", 0.05)
if civAir.ups < .0001 then civAir.ups = 0.05 end
end
if cfxZones.hasProperty(theZone, "maxTraffic") then
civAir.maxTraffic = cfxZones.getNumberFromZoneProperty(theZone, "maxTraffic", 10)
end
if cfxZones.hasProperty(theZone, "maxIdle") then
civAir.maxIdle = cfxZones.getNumberFromZoneProperty(theZone, "maxIdle", 8 * 60)
end
if cfxZones.hasProperty(theZone, "trafficRange") then
civAir.trafficRange = cfxZones.getNumberFromZoneProperty(theZone, "trafficRange", 120000) -- 120 km
end
if cfxZones.hasProperty(theZone, "ABPickMethod") then
civAir.ABPickMethod = cfxZones.getNumberFromZoneProperty(theZone, "ABPickMethod", 0) -- randomized any
end
if cfxZones.hasProperty(theZone, "initialAirSpawns") then
civAir.initialAirSpawns = cfxZones.getBoolFromZoneProperty(theZone, "initialAirSpawns", true)
end
function civAir.addPlane(thePlaneUnit) -- warning: is actually a group
if not thePlaneUnit then return end
civAir.activePlanes[thePlaneUnit:getName()] = thePlaneUnit
end
function civAir.removePlaneGroupByName(aName)
if not aName then
return
end
if civAir.activePlanes[aName] then
--trigger.action.outText("civA: REMOVING " .. aName .. " ***", 30)
civAir.activePlanes[aName] = nil
else
trigger.action.outText("civA: warning - ".. aName .." remove req but not found", 30)
end
end
function civAir.removePlane(thePlaneUnit) -- warning: is actually a group
if not thePlaneUnit then return end
if not thePlaneUnit:isExist() then return end
civAir.activePlanes[thePlaneUnit:getName()] = nil
end
function civAir.getPlane(aName) -- warning: returns GROUP!
return civAir.activePlanes[aName]
end
-- get an air base, may exclude an airbase from choice
-- method is dependent on
function civAir.getAnAirbase(excludeThisOne)
-- different methods to select a base
-- purely random from current list
local theAB;
if civAir.ABPickMethod == civAir.trafficAirbases.randomized then
repeat
local allAB = dcsCommon.getAirbasesWhoseNameContains("*", 0) -- all airfields, no Ships nor FABS
theAB = dcsCommon.pickRandom(allAB)
until theAB ~= excludeThisOne
return theAB
end
if civAir.ABPickMethod == civAir.trafficAirbases.localHubs then
-- first, pick a hub name
end
trigger.action.outText("civA: warning - unknown method <" .. civAir.ABPickMethod .. ">", 30)
return nil
end
function civAir.excludeAirbases(inList, excludeList)
if not inList then return {} end
if not excludeList then return inList end
if #excludeList < 1 then return inList end
local theDict = {}
-- build dict
for idx, aBase in pairs(inList) do
theDict[aBase:getName()] = aBase
end
-- now iterate through all excludes and remove them from dics
for idx, aName in pairs (excludeList) do
local allOfflimitAB = dcsCommon.getAirbasesWhoseNameContains(aName, 0)
for idx2, illegalBase in pairs (allOfflimitAB) do
theDict[illegalBase:getName()] = nil
end
end
-- now linearise (make array) from dict
local theArray = dcsCommon.enumerateTable(theDict)
return theArray
end
function civAir.getTwoAirbases()
local fAB
local sAB
-- get any two airbases on the map
if civAir.ABPickMethod == civAir.trafficAirbases.randomized then
local allAB = dcsCommon.getAirbasesWhoseNameContains("*", 0) -- all airfields, no Ships nor FABS, all coalitions
-- remove illegal source/dest airfields
allAB = civAir.excludeAirbases(allAB, civAir.excludeAirfields)
fAB = dcsCommon.pickRandom(allAB)
repeat
sAB = dcsCommon.pickRandom(allAB)
until fAB ~= sAB or (#allAB < 2)
return fAB, sAB
end
-- pick a hub, and then selct any two different airbases in the hub
if civAir.ABPickMethod == civAir.trafficAirbases.localHubs then
local hubName = dcsCommon.pickRandom(civAir.trafficCenters)
-- get the airfield that is identified by this
local theHub = dcsCommon.getFirstAirbaseWhoseNameContains(hubName, 0) -- only airfields, all coalitions
-- get all airbases that surround in range
local allAB = dcsCommon.getAirbasesInRangeOfAirbase(
theHub, -- centered on this base
true, -- include hub itself
civAir.trafficRange, -- hub size in meters
0 -- only airfields
)
allAB = civAir.excludeAirbases(allAB, civAir.excludeAirfields)
fAB = dcsCommon.pickRandom(allAB)
repeat
sAB = dcsCommon.pickRandom(allAB)
until fAB ~= sAB or (#allAB < 2)
return fAB, sAB
end
-- pick two hubs: one for source, one for destination airfields,
-- then pick an airfield from each hub
if civAir.ABPickMethod == civAir.trafficAirbases.betweenHubs then
--trigger.action.outText("between", 30)
local sourceHubName = dcsCommon.pickRandom(civAir.trafficCenters)
--trigger.action.outText("picked " .. sourceHubName, 30)
local sourceHub = dcsCommon.getFirstAirbaseWhoseNameContains(sourceHubName, 0)
--trigger.action.outText("sourceHub " .. sourceHub:getName(), 30)
local destHub
repeat destHubName = dcsCommon.pickRandom(civAir.trafficCenters)
until destHubName ~= sourceHubName or #civAir.trafficCenters < 2
destHub = dcsCommon.getFirstAirbaseWhoseNameContains(destHubName, 0)
--trigger.action.outText("destHub " .. destHub:getName(), 30)
local allAB = dcsCommon.getAirbasesInRangeOfAirbase(
sourceHub, -- centered on this base
true, -- include hub itself
civAir.trafficRange, -- hub size in meters
0 -- only airfields
)
allAB = civAir.excludeAirbases(allAB, civAir.excludeAirfields)
fAB = dcsCommon.pickRandom(allAB)
allAB = dcsCommon.getAirbasesInRangeOfAirbase(
destHub, -- centered on this base
true, -- include hub itself
civAir.trafficRange, -- hub size in meters
0 -- only airfields
)
allAB = civAir.excludeAirbases(allAB, civAir.excludeAirfields)
sAB = dcsCommon.pickRandom(allAB)
return fAB, sAB
end
trigger.action.outText("civA: warning - unknown method <" .. civAir.ABPickMethod .. "> in getTwoAirbases()", 30)
end
function civAir.parkingIsFree(fromWP)
-- iterate over all currently registres flights and make
-- sure that their location isn't closer than 10m to my new parking
local loc = {}
loc.x = fromWP.x
loc.y = fromWP.alt
loc.z = fromWP.z
for name, aPlaneGroup in pairs(civAir.activePlanes) do
if aPlaneGroup:isExist() then
local aPlane = aPlaneGroup:getUnit(1)
if aPlane:isExist() then
pos = aPlane:getPoint()
local delta = dcsCommon.dist(loc, pos)
if delta < 21 then
-- way too close
trigger.action.outText("civA: too close for comfort - " .. aPlane:getName() .. " occupies my slot", 30)
return false
end
end
end
end
return true
end
civAir.airStartSeparation = 0
function civAir.createFlight(name, theTypeString, fromAirfield, toAirfield, inAirStart)
if not fromAirfield then
trigger.action.outText("civA: NIL fromAirfield", 30)
return nil
end
if not toAirfield then
trigger.action.outText("civA: NIL toAirfield", 30)
return nil
end
local theGroup = dcsCommon.createEmptyAircraftGroupData (name)
local theAUnit = dcsCommon.createAircraftUnitData(name .. "-civA", theTypeString, false)
theAUnit.payload.fuel = 100000
dcsCommon.addUnitToGroupData(theAUnit, theGroup)
local fromWP = dcsCommon.createTakeOffFromParkingRoutePointData(fromAirfield)
if not fromWP then
trigger.action.outText("civA: fromWP create failed", 30)
return nil
end
if inAirStart then
-- modify WP into an in-air point
fromWP.alt = fromWP.alt + 3000 + civAir.airStartSeparation -- 9000 ft overhead + separation
fromWP.action = "Turning Point"
fromWP.type = "Turning Point"
fromWP.speed = 150;
fromWP.airdromeId = nil
theAUnit.alt = fromWP.alt
theAUnit.speed = fromWP.speed
end
-- sometimes, when landing kicks in too early, the plane lands
-- at the wrong airfield. AI sucks.
-- so we force overflight of target airfield
local overheadWP = dcsCommon.createOverheadAirdromeRoutPintData(toAirfield)
local toWP = dcsCommon.createLandAtAerodromeRoutePointData(toAirfield)
if not toWP then
trigger.action.outText("civA: toWP create failed", 30)
return nil
end
if not civAir.parkingIsFree(fromWP) then
trigger.action.outText("civA: failed free parking check for flight " .. name, 30)
return nil
end
dcsCommon.moveGroupDataTo(theGroup,
fromWP.x,
fromWP.y)
dcsCommon.addRoutePointForGroupData(theGroup, fromWP)
dcsCommon.addRoutePointForGroupData(theGroup, overheadWP)
dcsCommon.addRoutePointForGroupData(theGroup, toWP)
-- spawn
local groupCat = Group.Category.AIRPLANE
local theSpawnedGroup = coalition.addGroup(82, groupCat, theGroup) -- 82 is UN peacekeepers
return theSpawnedGroup
end
-- flightCount is a global that holds the number of flights we track
civAir.flightCount = 0
function civAir.createNewFlight(inAirStart)
civAir.flightCount = civAir.flightCount + 1
local fAB, sAB = civAir.getTwoAirbases() -- from AB
local name = fAB:getName() .. "-" .. sAB:getName().. "/" .. civAir.flightCount
local TypeString = dcsCommon.pickRandom(civAir.aircraftTypes)
local theFlight = civAir.createFlight(name, TypeString, fAB, sAB, inAirStart)
if not theFlight then
-- flight was not able to spawn.
trigger.action.outText("civA: aborted civ spawn on fAB:" .. fAB:getName(), 30)
return
end
civAir.addPlane(theFlight) -- track it
if civAir.verbose then
trigger.action.outText("civA: created flight from <" .. fAB:getName() .. "> to <" .. sAB:getName() .. ">", 30)
end
end
function civAir.airStartPopulation()
local numAirStarts = civAir.maxTraffic / 2
civAir.airStartSeparation = 0
while numAirStarts > 0 do
numAirStarts = numAirStarts - 1
civAir.airStartSeparation = civAir.airStartSeparation + 200
civAir.createNewFlight(true)
end
end
--
-- U P D A T E L O O P
--
function civAir.update()
-- reschedule me in the future. ups = updates per second.
timer.scheduleFunction(civAir.update, {}, timer.getTime() + 1/civAir.ups)
-- clean-up first:
-- any group that no longer exits will be removed from the array
local removeMe = {}
for name, group in pairs (civAir.activePlanes) do
if not group:isExist() then
table.insert(removeMe, name) -- mark for deletion
--Group.destroy(group) -- may break
end
end
for idx, name in pairs(removeMe) do
civAir.activePlanes[name] = nil
trigger.action.outText("civA: warning - removed " .. name .. " from active roster, no longer exists", 30)
end
-- now, run through all existing flights and update their
-- idle times. also count how many planes there are
local planeNum = 0
local overduePlanes = {}
local now = timer.getTime()
for name, aPlaneGroup in pairs(civAir.activePlanes) do
local speed = 0
if aPlaneGroup:isExist() then
local aPlane = aPlaneGroup:getUnit(1)
if aPlane and aPlane:isExist() and aPlane:getLife() >= 1 then
planeNum = planeNum + 1
local vel = aPlane:getVelocity()
speed = dcsCommon.mag(vel.x, vel.y, vel.z)
else
-- force removal of group
civAir.idlePlanes[name] = -1000
speed = 0
end
else
-- force removal
civAir.idlePlanes[name] = -1000
speed = 0
end
if speed < 0.5 then
if not civAir.idlePlanes[name] then
civAir.idlePlanes[name] = now
end
local idleTime = now - civAir.idlePlanes[name]
--trigger.action.outText("civA: Idling <" .. name .. "> for t=" .. idleTime, 30)
if idleTime > civAir.maxIdle then
table.insert(overduePlanes, name)
end
else
-- zero out idle plane
civAir.idlePlanes[name] = nil
end
--]]--
end
-- see if we have less than max flights running
if planeNum < civAir.maxTraffic then
-- spawn a new plane. just one per pass
civAir.createNewFlight()
end
-- now remove all planes that are overdue
for idx, aName in pairs(overduePlanes) do
local aFlight = civAir.getPlane(aName) -- returns a group
civAir.removePlaneGroupByName(aName) -- remove from roster
if aFlight and aFlight:isExist() then
-- destroy can only work if group isexist!
Group.destroy(aFlight) -- remember: flights are groups!
end
end
end
function civAir.doDebug(any)
trigger.action.outText("cf/x civTraffic debugger.", 30)
local desc = "Active Planes:"
local now = timer.getTime()
for name, group in pairs (civAir.activePlanes) do
desc = desc .. "\n" .. name
if civAir.idlePlanes[name] then
delay = now - civAir.idlePlanes[name]
desc = desc .. " (idle for " .. delay .. ")"
end
end
trigger.action.outText(desc, 30)
end
function civAir.collectHubs()
local pZones = cfxZones.zonesWithProperty("civAir")
for k, aZone in pairs(pZones) do
local value = cfxZones.getStringFromZoneProperty(aZone, "civAir", "")
local af = dcsCommon.getClosestAirbaseTo(aZone.point, 0) -- 0 = only airfields, not farp or ships
if af then
local afName = af:getName()
if value:lower() == "exclude" then
table.insert(civAir.excludeAirfields, afName)
else
table.insert(civAir.trafficCenters, afName)
end
end
end
end
function civAir.listTrafficCenters()
trigger.action.outText("Traffic Centers", 30)
for idx, aName in pairs(civAir.trafficCenters) do
trigger.action.outText(aName, 30)
end
end
-- start
function civAir.start()
-- module check
if not dcsCommon.libCheck("cfx civAir", civAir.requiredLibs) then
return false
end
-- see if there is a config zone and load it
civAir.readConfigZone()
-- look for zones to add to air fields list
civAir.collectHubs()
-- make sure there is something in trafficCenters
if #civAir.trafficCenters < 1 then
trigger.action.outText("+++civTraffic: auto-populating", 30)
-- simply add airfields on the map
local allBases = dcsCommon.getAirbasesWhoseNameContains("*", 0)
for idx, aBase in pairs(allBases) do
local afName = aBase:getName()
--trigger.action.outText("+++civTraffic: adding " .. afName, 30)
table.insert(civAir.trafficCenters, afName)
end
end
civAir.listTrafficCenters()
-- air-start half population if allowed
if civAir.initialAirSpawns then
civAir.airStartPopulation()
end
-- start the update loop
civAir.update()
-- say hi!
trigger.action.outText("cf/x civTraffic v" .. civAir.version .. " started.", 30)
return true
end
if not civAir.start() then
trigger.action.outText("cf/x civAir aborted: missing libraries", 30)
civAir = nil
end
--[[--
Additional ideas
source to target method
--]]--

1151
modules/csarManager2.lua Normal file

File diff suppressed because it is too large Load Diff

2063
modules/dcsCommon.lua Normal file

File diff suppressed because it is too large Load Diff

515
modules/guardianAngel.lua Normal file
View File

@ -0,0 +1,515 @@
guardianAngel = {}
guardianAngel.version = "2.0.2"
guardianAngel.ups = 10
guardianAngel.launchWarning = true -- detect launches and warn pilot
guardianAngel.intervention = true -- remove missiles just before hitting
guardianAngel.explosion = -1 -- small poof when missile explodes. -1 = off.
guardianAngel.verbose = false -- debug info
guardianAngel.announcer = true -- angel talks to you
guardianAngel.private = false -- angel only talks to group
guardianAngel.autoAddPlayers = true
guardianAngel.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
}
--[[--
Version History
1.0.0 - Initial version
2.0.0 - autoAddPlayer
- verbose
- lib check
- config zone
- sneaky detection logic
- god intervention 100m
- detect re-acquisition
- addUnitToWatch supports string names
- detect non-miss
- announcer
- intervene optional
- launch warning optional
2.0.1 - warnings go to group
- invokeCallbacks added to module
- reworked CB structure
- private option
2.0.2 - poof! explosion option to show explosion on intervention
- can be dangerous
This script detects missiles launched against protected aircraft an
removes them when they are about to hit
--]]--
guardianAngel.minMissileDist = 50 -- m. below this distance the missile is killed by god, not the angel :)
guardianAngel.myEvents = {1, 15, 20, 21, 23} -- 1 - shot, 15 - birth, 20 - enter unit, 21 - player leave unit, 23 - start shooting
guardianAngel.safetyFactor = 1.8 -- for calculating dealloc range
guardianAngel.unitsToWatchOver = {} -- I'll watch over these
guardianAngel.missilesInTheAir = {} -- missiles in the air
guardianAngel.callBacks = {} -- callbacks
-- callback signature: callBack(reason, targetUnitName, weaponName)
-- reasons (string): "launch", "miss", "reacquire", "trackloss", "disappear", "intervention"
function guardianAngel.addCallback(theCallback)
if theCallback then
table.insert(guardianAngel.callBacks, theCallback)
end
end
function guardianAngel.invokeCallbacks(reason, targetName, weaponName)
for idx, theCB in pairs(guardianAngel.callBacks) do
theCB(reason, targetName, weaponName)
end
end
--
-- units to watch
--
function guardianAngel.addUnitToWatch(aUnit)
if type(aUnit) == "string" then
aUnit = Unit.getByName(aUnit)
end
if not aUnit then return end
local unitName = aUnit:getName()
guardianAngel.unitsToWatchOver[unitName] = aUnit
if guardianAngel.verbose then
trigger.action.outText("+++gA: now watching unit " .. aUnit:getName(), 30)
end
end
function guardianAngel.removeUnitToWatch(aUnit)
if type(aUnit) == "string" then
aUnit = Unit.getByName(aUnit)
end
if not aUnit then return end
local unitName = aUnit:getName()
if not unitName then return end
guardianAngel.unitsToWatchOver[unitName] = nil
if guardianAngel.verbose then
trigger.action.outText("+++gA: no longer watching " .. aUnit:getName(), 30)
end
end
function guardianAngel.getWatchedUnitByName(aName)
if not aName then return nil end
return guardianAngel.unitsToWatchOver[aName]
end
--
-- watch q items
--
function guardianAngel.createQItem(theWeapon, theTarget, detectProbability)
if not theWeapon then return nil end
if not theTarget then return nil end
if not theTarget:isExist() then return nil end
if not detectProbability then detectProbability = 1.0 end
local theItem = {}
theItem.theWeapon = theWeapon
theItem.wP = theWeapon:getPoint() -- save location
theItem.weaponName = theWeapon:getName()
theItem.theTarget = theTarget
theItem.tGroup = theTarget:getGroup()
theItem.tID = theItem.tGroup:getID()
theItem.targetName = theTarget:getName()
theItem.launchTimeStamp = timer.getTime()
theItem.lastCheckTimeStamp = -1000
theItem.lastDistance = math.huge
theItem.detected = false
theItem.lostTrack = false -- so we can detect sneakies!
theItem.missed = false -- just keep watching for re-ack
return theItem
end
--[[--
function guardianAngel.detectItem(theItem)
if theItem.detected then return end
-- perform detection calculations here
end
--]]--
-- calculate a point in direction from plane (pln) to weapon (wpn), dist meters
function guardianAngel.calcSafeExplosionPoint(wpn, pln, dist)
local dirToWpn = dcsCommon.vSub(wpn, pln) -- vector to weapon.
local v = dcsCommon.vNorm(dirToWpn) -- |v| = 1
local v = dcsCommon.vMultScalar(v, dist) -- |v| = dist
local newPoint = dcsCommon.vAdd(pln, v)
return newPoint
end
function guardianAngel.monitorItem(theItem)
local w = theItem.theWeapon
local ID = theItem.tID
if not w then return false end
if not w:isExist() then
if (not theItem.missed) and (not theItem.lostTrack) then
local desc = theItem.weaponName .. ": DISAPPEARED"
if guardianAngel.announcer then
if guardianAngel.private then
trigger.action.outTextForGroup(ID, desc, 30)
else
trigger.action.outText(desc, 30)
end
end
guardianAngel.invokeCallbacks("disappear", theItem.targetName, theItem.weaponName)
end
return false
end
local t = theItem.theTarget
local currentTarget = w:getTarget()
local oldWPos = theItem.wP
local A = w:getPoint() -- A is new point of weapon
theItem.wp = A -- update new position, old is in oldWPos
local B
if currentTarget then B = currentTarget:getPoint() else B = A end
local d = math.floor(dcsCommon.dist(A, B))
local desc = theItem.weaponName .. ": "
if t == currentTarget then
desc = desc .. "tracking " .. theItem.targetName .. ", d = " .. d .. "m"
local vcc = dcsCommon.getClosingVelocity(t, w)
desc = desc .. ", Vcc = " .. math.floor(vcc) .. "m/s"
-- now calculate lethal distance: vcc is in meters per second
-- and we sample ups times per second
-- making the missile cover vcc / ups meters in the next
-- timer interval. If it now is closer than that, we have to
-- destroy the missile
local lethalRange = math.abs(vcc / guardianAngel.ups) * guardianAngel.safetyFactor
desc = desc .. ", LR= " .. math.floor(lethalRange) .. "m"
if guardianAngel.intervention and
d <= lethalRange + 10
then
desc = desc .. " ANGEL INTERVENTION"
if theItem.lostTrack then desc = desc .. " (little sneak!)" end
if theItem.missed then desc = desc .. " (missed you!)" end
if guardianAngel.announcer then
if guardianAngel.private then
trigger.action.outTextForGroup(ID, desc, 30)
else
trigger.action.outText(desc, 30)
end
end
guardianAngel.invokeCallbacks("intervention", theItem.targetName, theItem.weaponName)
w:destroy()
-- now add some showy explosion so the missile
-- doesn't just disappear
if guardianAngel.explosion > 0 then
local xP = guardianAngel.calcSafeExplosionPoint(A,B, 500)
trigger.action.explosion(xP, guardianAngel.explosion)
end
return false -- remove from list
end
if guardianAngel.intervention and
d <= guardianAngel.minMissileDist -- god's override
then
desc = desc .. " GOD INTERVENTION"
if theItem.lostTrack then desc = desc .. " (little sneak!)" end
if theItem.missed then desc = desc .. " (missed you!)" end
if guardianAngel.announcer then
if guardianAngel.private then
trigger.action.outTextForGroup(ID, desc, 30)
else
trigger.action.outText(desc, 30)
end
end
guardianAngel.invokeCallbacks("intervention", theItem.targetName, theItem.weaponName)
w:destroy()
if guardianAngel.explosion > 0 then
local xP = guardianAngel.calcSafeExplosionPoint(A,B, 500)
trigger.action.explosion(xP, guardianAngel.explosion)
end
return false -- remove from list
end
else
if not theItem.lostTrack then
desc = desc .. "Missile LOST TRACK"
if guardianAngel.announcer then
if guardianAngel.private then
trigger.action.outTextForGroup(ID, desc, 30)
else
trigger.action.outText(desc, 30)
end
end
guardianAngel.invokeCallbacks("trackloss", theItem.targetName, theItem.weaponName)
theItem.lostTrack = true
end
theItem.lastDistance = d
return true -- true because they can re-acquire!
end
if d > theItem.lastDistance then
-- this can be wrong because if a missile is launched
-- at an angle, it can initially look as if it missed
if not theItem.missed then
desc = desc .. " Missile MISSED!"
if guardianAngel.announcer then
if guardianAngel.private then
trigger.action.outTextForGroup(ID, desc, 30)
else
trigger.action.outText(desc, 30)
end
end
guardianAngel.invokeCallbacks("miss", theItem.targetName, theItem.weaponName)
theItem.missed = true
end
theItem.lastDistance = d
return true -- better not disregard - they can re-acquire!
end
if theItem.missed and d < theItem.lastDistance then
desc = desc .. " Missile RE-ACQUIRED!"
if guardianAngel.announcer then
if guardianAngel.private then
trigger.action.outTextForGroup(ID, desc, 30)
else
trigger.action.outText(desc, 30)
end
end
theItem.missed = false
guardianAngel.invokeCallbacks("reacquire", theItem.targetName, theItem.weaponName)
end
theItem.lastDistance = d
return true
end
function guardianAngel.monitorMissiles()
local newArray = {} -- we collect all still existing missiles here
-- and replace missilesInTheAir with that for next round
for idx, anItem in pairs (guardianAngel.missilesInTheAir) do
-- we now have an item
-- see about detection
-- guardianAngel.detectItem(anItem)
-- see if the weapon is still in existence
stillAlive = guardianAngel.monitorItem(anItem)
if stillAlive then
table.insert(newArray, anItem)
end
end
guardianAngel.missilesInTheAir = newArray
end
--
-- E V E N T P R O C E S S I N G
--
function guardianAngel.isInteresting(eventID)
-- return true if we are interested in this event, false else
for key, evType in pairs(guardianAngel.myEvents) do
if evType == eventID then return true end
end
return false
end
-- event pre-proc: only return true if we need to process this event
function guardianAngel.preProcessor(event)
-- all events must have initiator set
if not event.initiator then return false end
-- see if the event ID is interesting for us
local interesting = guardianAngel.isInteresting(event.id)
return interesting
end
function guardianAngel.postProcessor(event)
-- don't do anything for now
end
-- event callback from dcsCommon event handler. preProcessor has returned true
function guardianAngel.somethingHappened(event)
-- when this is invoked, the preprocessor guarantees that
-- it's an interesting event and has initiator
local ID = event.id
local theUnit = event.initiator
local playerName = theUnit:getPlayerName() -- nil if not a player
if ID == 15 and playerName then
-- this is a player created unit
if guardianAngel.verbose then
trigger.action.outText("+++gA: unit born " .. theUnit:getName(), 30)
end
if guardianAngel.autoAddPlayers then
guardianAngel.addUnitToWatch(theUnit)
end
return
end
if ID == 20 and playerName then
-- this is a player entered unit
if guardianAngel.verbose then
trigger.action.outText("+++gA: player seated in unit " .. theUnit:getName(), 30)
end
if guardianAngel.autoAddPlayers then
guardianAngel.addUnitToWatch(theUnit)
end
return
end
if ID == 21 and playerName then
guardianAngel.removeUnitToWatch(theUnit)
return
end
if ID == 15 or ID == 20 or ID == 21 then
-- non-player events of same type, disregard
return
end
if ID == 1 then
-- someone shot something. see if it is fire directed at me
local theWeapon = event.weapon
local theTarget
if theWeapon then
theTarget = theWeapon:getTarget()
else
return
end
if not theTarget then
return
end
if not theTarget:isExist() then return end
-- if we get here, we have weapon aimed at a target
local targetName = theTarget:getName()
local watchedUnit = guardianAngel.getWatchedUnitByName(targetName)
if not watchedUnit then return end -- fired at some other poor sucker, we don't care
-- if we get here, someone fired a guided weapon at my watched units
-- create a new item for my queue
local theQItem = guardianAngel.createQItem(theWeapon, theTarget) -- prob 100
table.insert(guardianAngel.missilesInTheAir, theQItem)
guardianAngel.invokeCallbacks("launch", theQItem.targetName, theQItem.weaponName)
local unitHeading = dcsCommon.getUnitHeadingDegrees(theTarget)
local A = theWeapon:getPoint()
local B = theTarget:getPoint()
local oclock = dcsCommon.clockPositionOfARelativeToB(A, B, unitHeading)
local grpID = theTarget:getGroup():getID()
if guardianAngel.launchWarning then
-- currently, we always detect immediately
-- can be moved to update()
if guardianAngel.private then
trigger.action.outTextForGroup(grpID, "Missile, missile, missile, " .. oclock .. " o clock", 30)
else
trigger.action.outText("Missile, missile, missile, " .. oclock .. " o clock", 30)
end
theQItem.detected = true -- remember: we detected and warned already
end
return
end
local myType = theUnit:getTypeName()
if guardianAngel.verbose then
trigger.action.outText("+++gA: event " .. ID .. " for unit " .. theUnit:getName() .. " of type " .. myType, 30)
end
end
--
-- U P D A T E L O O P
--
function guardianAngel.update()
timer.scheduleFunction(guardianAngel.update, {}, timer.getTime() + 1/guardianAngel.ups)
guardianAngel.monitorMissiles()
end
function guardianAngel.collectPlayerUnits()
-- make sure we have all existing player units
-- at start of game
if not guardianAngel.autoAddPlayer then return end
for i=1, 2 do
-- currently only two factions in dcs
factionUnits = coalition.getPlayers(i)
for idx, aPlayerUnit in pairs(factionUnits) do
-- add all existing faction units
guardianAngel.addUnitToWatch(aPlayerUnit)
end
end
end
--
-- config reading
--
function guardianAngel.readConfigZone()
-- note: must match exactly!!!!
local theZone = cfxZones.getZoneByName("guardianAngelConfig")
if not theZone then
trigger.action.outText("+++gA: no config zone!", 30)
return
end
if guardianAngel.verbose then
trigger.action.outText("+++gA: found config zone!", 30)
end
guardianAngel.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
guardianAngel.autoAddPlayer = cfxZones.getBoolFromZoneProperty(theZone, "autoAddPlayer", true)
guardianAngel.launchWarning = cfxZones.getBoolFromZoneProperty(theZone, "launchWarning", true)
guardianAngel.intervention = cfxZones.getBoolFromZoneProperty(theZone, "intervention", true)
guardianAngel.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true)
guardianAngel.private = cfxZones.getBoolFromZoneProperty(theZone, "private", false)
guardianAngel.explosion = cfxZones.getNumberFromZoneProperty(theZone, "explosion", -1)
end
--
-- start
--
function guardianAngel.start()
-- lib check
if not dcsCommon.libCheck("cfx Guardian Angel",
guardianAngel.requiredLibs) then
return false
end
-- read config
guardianAngel.readConfigZone()
-- install event monitor
dcsCommon.addEventHandler(guardianAngel.somethingHappened,
guardianAngel.preProcessor,
guardianAngel.postProcessor)
-- collect all units that are already in the game at this point
guardianAngel.collectPlayerUnits()
-- start update
guardianAngel.update()
trigger.action.outText("Guardian Angel v" .. guardianAngel.version .. " running", 30)
return true
end
function guardianAngel.testCB(reason, targetName, weaponName)
trigger.action.outText("gA - CB for ".. reason .. ": " .. targetName .. " w: " .. weaponName, 30)
end
-- go go go
if not guardianAngel.start() then
trigger.action.outText("Loading Guardian Angel failed.", 30)
guardianAngel = nil
end
-- test callback
--guardianAngel.addCallback(guardianAngel.testCB)
--guardianAngel.invokeCallbacks("A", "B", "C")

395
modules/jtacGrpUI.lua Normal file
View File

@ -0,0 +1,395 @@
jtacGrpUI = {}
jtacGrpUI.version = "1.0.2"
--[[-- VERSION HISTORY
- 1.0.2 - also include idling JTACS
- add positional info when using owned zones
--]]--
-- find & command cfxGroundTroops-based jtacs
-- UI installed via OTHER for all groups with players
-- module based on xxxGrpUI
jtacGrpUI.groupConfig = {} -- all inited group private config data
jtacGrpUI.simpleCommands = true -- if true, f10 other invokes directly
--
-- C O N F I G H A N D L I N G
-- =============================
--
-- Each group has their own config block that can be used to
-- store group-private data and configuration items.
--
function jtacGrpUI.resetConfig(conf)
end
function jtacGrpUI.createDefaultConfig(theGroup)
local conf = {}
conf.theGroup = theGroup
conf.name = theGroup:getName()
conf.id = theGroup:getID()
conf.coalition = theGroup:getCoalition()
jtacGrpUI.resetConfig(conf)
conf.mainMenu = nil; -- this is where we store the main menu if we branch
conf.myCommands = nil; -- this is where we store the commands if we branch
return conf
end
-- getConfigFor group will allocate if doesn't exist in DB
-- and add to it
function jtacGrpUI.getConfigForGroup(theGroup)
if not theGroup then
trigger.action.outText("+++WARNING: jtacGrpUI nil group in getConfigForGroup!", 30)
return nil
end
local theName = theGroup:getName()
local c = jtacGrpUI.getConfigByGroupName(theName) -- we use central accessor
if not c then
c = jtacGrpUI.createDefaultConfig(theGroup)
jtacGrpUI.groupConfig[theName] = c -- should use central accessor...
end
return c
end
function jtacGrpUI.getConfigByGroupName(theName) -- DOES NOT allocate when not exist
if not theName then return nil end
return jtacGrpUI.groupConfig[theName]
end
function jtacGrpUI.getConfigForUnit(theUnit)
-- simple one-off step by accessing the group
if not theUnit then
trigger.action.outText("+++WARNING: jtacGrpUI nil unit in getConfigForUnit!", 30)
return nil
end
local theGroup = theUnit:getGroup()
return getConfigForGroup(theGroup)
end
--
--
-- M E N U H A N D L I N G
-- =========================
--
--
function jtacGrpUI.clearCommsSubmenus(conf)
if conf.myCommands then
for i=1, #conf.myCommands do
missionCommands.removeItemForGroup(conf.id, conf.myCommands[i])
end
end
conf.myCommands = {}
end
function jtacGrpUI.removeCommsFromConfig(conf)
jtacGrpUI.clearCommsSubmenus(conf)
if conf.myMainMenu then
missionCommands.removeItemForGroup(conf.id, conf.myMainMenu)
conf.myMainMenu = nil
end
end
-- this only works in single-unit groups. may want to check if group
-- has disappeared
function jtacGrpUI.removeCommsForUnit(theUnit)
if not theUnit then return end
if not theUnit:isExist() then return end
-- perhaps add code: check if group is empty
local conf = jtacGrpUI.getConfigForUnit(theUnit)
jtacGrpUI.removeCommsFromConfig(conf)
end
function jtacGrpUI.removeCommsForGroup(theGroup)
if not theGroup then return end
if not theGroup:isExist() then return end
local conf = jtacGrpUI.getConfigForGroup(theGroup)
jtacGrpUI.removeCommsFromConfig(conf)
end
--
-- set main root in F10 Other. All sub menus click into this
--
function jtacGrpUI.isEligibleForMenu(theGroup)
return true
end
function jtacGrpUI.setCommsMenuForUnit(theUnit)
if not theUnit then
trigger.action.outText("+++WARNING: jtacGrpUI nil UNIT in setCommsMenuForUnit!", 30)
return
end
if not theUnit:isExist() then return end
local theGroup = theUnit:getGroup()
jtacGrpUI.setCommsMenu(theGroup)
end
function jtacGrpUI.setCommsMenu(theGroup)
-- depending on own load state, we set the command structure
-- it begins at 10-other, and has 'jtac' as main menu with submenus
-- as required
if not theGroup then return end
if not theGroup:isExist() then return end
-- we test here if this group qualifies for
-- the menu. if not, exit
if not jtacGrpUI.isEligibleForMenu(theGroup) then return end
local conf = jtacGrpUI.getConfigForGroup(theGroup)
conf.id = theGroup:getID(); -- we do this ALWAYS so it is current even after a crash
-- trigger.action.outText("+++ setting group <".. conf.theGroup:getName() .. "> jtac command", 30)
if jtacGrpUI.simpleCommands then
-- we install directly in F-10 other
if not conf.myMainMenu then
local commandTxt = "jtac Lasing Report"
local theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
nil,
jtacGrpUI.redirectCommandX,
{conf, "lasing report"}
)
conf.myMainMenu = theCommand
end
return
end
-- ok, first, if we don't have an F-10 menu, create one
if not (conf.myMainMenu) then
conf.myMainMenu = missionCommands.addSubMenuForGroup(conf.id, 'jtac')
end
-- clear out existing commands
jtacGrpUI.clearCommsSubmenus(conf)
-- now we have a menu without submenus.
-- add our own submenus
jtacGrpUI.addSubMenus(conf)
end
function jtacGrpUI.addSubMenus(conf)
-- add menu items to choose from after
-- user clickedf on MAIN MENU. In this implementation
-- they all result invoked methods
local commandTxt = "jtac Lasing Report"
local theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myMainMenu,
jtacGrpUI.redirectCommandX,
{conf, "lasing report"}
)
table.insert(conf.myCommands, theCommand)
--[[--
commandTxt = "This is another important command"
theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myMainMenu,
jtacGrpUI.redirectCommandX,
{conf, "Sub2"}
)
table.insert(conf.myCommands, theCommand)
--]]--
end
--
-- each menu item has a redirect and timed invoke to divorce from the
-- no-debug zone in the menu invocation. Delay is .1 seconds
--
function jtacGrpUI.redirectCommandX(args)
timer.scheduleFunction(jtacGrpUI.doCommandX, args, timer.getTime() + 0.1)
end
function jtacGrpUI.doCommandX(args)
local conf = args[1] -- < conf in here
local what = args[2] -- < second argument in here
local theGroup = conf.theGroup
-- trigger.action.outTextForGroup(conf.id, "+++ groupUI: processing comms menu for <" .. what .. ">", 30)
local targetList = jtacGrpUI.collectJTACtargets(conf, true)
-- iterate the list
if #targetList < 1 then
trigger.action.outTextForGroup(conf.id, "No targets are currently being lased", 30)
return
end
local desc = "JTAC Target Report:\n"
-- trigger.action.outTextForGroup(conf.id, "Target Report:", 30)
for i=1, #targetList do
local aTarget = targetList[i]
if aTarget.idle then
desc = desc .. "\n" .. aTarget.jtacName .. aTarget.posInfo ..": no target"
else
desc = desc .. "\n" .. aTarget.jtacName .. aTarget.posInfo .." lasing " .. aTarget.lazeTargetType .. " [" .. aTarget.range .. "nm at " .. aTarget.bearing .. "°]"
end
end
trigger.action.outTextForGroup(conf.id, desc .. "\n", 30)
end
function jtacGrpUI.collectJTACtargets(conf, includeIdle)
-- iterate cfxGroundTroops.deployedTroops to retrieve all
-- troops that are lazing. 'Lazing' are all groups that
-- have an active (non-nil) lazeTarget and 'laze' orders
if not includeIdle then includeIdle = false end
local theJTACS = {}
for idx, troop in pairs(cfxGroundTroops.deployedTroops) do
if troop.coalition == conf.coalition
and troop.orders == "laze"
and troop.lazeTarget
and troop.lazeTarget:isExist()
then
table.insert(theJTACS, troop)
elseif troop.coalition == conf.coalition
and troop.orders == "laze"
and includeIdle
then
-- we also include idlers
table.insert(theJTACS, troop)
end
end
-- we now have a list of all ground troops that are lazing.
-- get bearing and range to targets, and sort them accordingly
local targetList = {}
local here = dcsCommon.getGroupLocation(conf.theGroup) -- this is me
for idx, troop in pairs (theJTACS) do
local aTarget = {}
-- establish our location
aTarget.jtacName = troop.name
aTarget.posInfo = ""
if cfxOwnedZones and cfxOwnedZones.hasOwnedZones() then
local jtacLoc = dcsCommon.getGroupLocation(troop.group)
local nearestZone = cfxOwnedZones.getNearestOwnedZoneToPoint(jtacLoc)
if nearestZone then
local ozRange = dcsCommon.dist(jtacLoc, nearestZone.point) * 0.000621371
ozRange = math.floor(ozRange * 10) / 10
local relPos = dcsCommon.compassPositionOfARelativeToB(jtacLoc, nearestZone.point)
aTarget.posInfo = " (" .. ozRange .. "nm " .. relPos .. " of " .. nearestZone.name .. ")"
end
end
-- we may get idlers, catch them now
if not troop.lazeTarget then
aTarget.idle = true
aTarget.range = math.huge
else
-- get the target we are lazing
local there = troop.lazeTarget:getPoint()
aTarget.idle = false
aTarget.range = dcsCommon.dist(here, there)
aTarget.range = aTarget.range * 0.000621371 -- meter to miles
aTarget.range = math.floor(aTarget.range * 10) / 10
aTarget.bearing = dcsCommon.bearingInDegreesFromAtoB(here, there)
--aTarget.jtacName = troop.name
aTarget.lazeTargetType = troop.lazeTargetType
end
table.insert(targetList, aTarget)
end
-- now sort by range
table.sort(targetList, function (left, right) return left.range < right.range end )
-- return list sorted by distance
return targetList
end
--
-- G R O U P M A N A G E M E N T
--
-- Group Management is required to make sure all groups
-- receive a comms menu and that they receive a clean-up
-- when required
--
-- Callbacks are provided by cfxPlayer module to which we
-- subscribe during init
--
function jtacGrpUI.playerChangeEvent(evType, description, player, data)
--trigger.action.outText("+++ groupUI: received <".. evType .. "> Event", 30)
if evType == "newGroup" then
-- initialized attributes are in data as follows
-- .group - new group
-- .name - new group's name
-- .primeUnit - the unit that trigggered new group appearing
-- .primeUnitName - name of prime unit
-- .id group ID
--theUnit = data.primeUnit
jtacGrpUI.setCommsMenu(data.group)
-- trigger.action.outText("+++ groupUI: added " .. theUnit:getName() .. " to comms menu", 30)
return
end
if evType == "removeGroup" then
-- data is the player record that no longer exists. it consists of
-- .name
-- we must remove the comms menu for this group else we try to add another one to this group later
local conf = jtacGrpUI.getConfigByGroupName(data.name)
if conf then
jtacGrpUI.removeCommsFromConfig(conf) -- remove menus
jtacGrpUI.resetConfig(conf) -- re-init this group for when it re-appears
else
trigger.action.outText("+++ jtacUI: can't retrieve group <" .. data.name .. "> config: not found!", 30)
end
return
end
if evType == "leave" then
-- player unit left. we don't care since we only work on group level
-- if they were the only, this is followed up by group disappeared
end
if evType == "unit" then
-- player changed units. almost never in MP, but possible in solo
-- because of 1 seconds timing loop
-- will result in a new group appearing and a group disappearing, so we are good
-- may need some logic to clean up old configs and/or menu items
end
end
--
-- Start
--
function jtacGrpUI.start()
-- iterate existing groups so we have a start situation
-- 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 player record. Does not resolve on unit level!
for gname, pgroup in pairs(allPlayerGroups) do
local theUnit = pgroup.primeUnit -- get any unit of that group
jtacGrpUI.setCommsMenuForUnit(theUnit) -- set up
end
-- now install the new group notifier to install Assault Troops menu
cfxPlayer.addMonitor(jtacGrpUI.playerChangeEvent)
trigger.action.outText("cf/x jtacGrpUI v" .. jtacGrpUI.version .. " started", 30)
end
--
-- GO GO GO
--
if not cfxGroundTroops then
trigger.action.outText("cf/x jtacGrpUI REQUIRES cfxGroundTroops to work.", 30)
else
jtacGrpUI.start()
end

View File

@ -0,0 +1,828 @@
limitedAirframes = {}
limitedAirframes.version = "1.3.0"
limitedAirframes.enabled = true -- can be turned off
limitedAirframes.userCanToggle = true -- F-10 menu?
limitedAirframes.maxRed = -1 -- -1 == infinite
limitedAirframes.maxBlue = 6 -- -1 = infinite
limitedAirframes.redWinsFlag = "999"
limitedAirframes.blueWinsFlag = "998"
limitedAirframes.requiredLibs = {
"dcsCommon", -- common is of course needed for everything
-- pretty stupid to check for this since we
-- need common to invoke the check, but anyway
"cfxZones", -- Zones, of course for safe landings
"cfxPlayer", -- callbacks
-- "cfxGroundTroops", -- generic data module for weight
}
--[[-- VERSION HISTORY
- 1.0.0 - initial version
- 1.0.1 - out text to coalition for switch
- less verbose
- win/lose sound by coalition
- 1.0.2 - corrected some to to-->for Groups typos
- 1.0.3 - renamed to 'pilot' instead of airframe:
pilotSafe attribute
- fixed MP bug, switched to EventMonII code
- 1.0.4 - added CSAR integration: create CSAR and callback
- safe ditch only at less than 7 kmh and 10m agl
- 1.0.5 - replaced 5 (crash) check for helos only
- 1.0.6 - changed alt and speed tests to inAir
reduced verbosity
made reporting of new units for coalition side only
- 1.0.7 - if unlimited pilots it says so when you return one
to base
- 1.0.8 - now can query remaining pilots
- 1.0.9 - better formatted remaining pilots
- 1.1.0 - module manager
- separated out settings
- hand change in pilotsafe zones that can be landed in
- 1.2.0 - limitedAirframesConfig zone
- 1.3.0 - added network dead override logic via unitFlownByPlayer
--]]--
-- limitedAirframes manages the number of available player airframes
-- per scenario and side. Each time a player crashes the plane
-- outside of safe zones, the number is decreased for that side
-- when the number reaches -1 or smaller, other side wins
-- !!!Only affects player planes!!
-- *** EXTENDS ZONES ***
-- safe zones must have a property "pilotSafe"
-- - pilotSafe - this is a zone to safely change airframes in
-- - redSafe (optional, defaults to true)
-- - blueSafe (optional, defaults to true)
-- set to "false" or "no" to disallow that side to change
-- airframes even when safer
-- if zone can change ownership, player's coalition
-- is checked against current zone ownership
-- zone owner.
-- when red wins due to blue frame loss, flag 999 is set to true
-- when blue wins due to red frame loss, flag 998 is set to true
-- set a mission trigger to end mission if you want to end mission
-- or simply keep running, and a CHEATER! message will flash
-- every time the losing side enters a new aircraft
limitedAirframes.safeZones = {} -- safezones are zones where a crash or change plane does not
-- these zones are created by adding an 'pilotSafe' attribute
limitedAirframes.myEvents = {5, 9, 30, 6, 20, 21, 15 } -- 5 = crash, 9 - dead, 30 - unit lost, 6 - eject, 20 - enter unit, 21 - leave unit, 15 - birth
-- guarantee a min of 2 seconds between events
-- for this we save last event per player
limitedAirframes.lastEvents = {}
-- each time a plane crashes or is abandoned check
-- that it's a player unit
-- inside a crash free zone
-- update the side's airframe credit
limitedAirframes.currRed = 0
limitedAirframes.currRed = 0
-- we record all unit names that contain a player
-- so that we can check against these when we receive
-- an ejection event. We also keep a list of players
-- for good measure and their status
limitedAirframes.playerUnits = {}
limitedAirframes.players = {}
limitedAirframes.unitFlownByPlayer = {} -- to detect dead after
-- 21 (player left unit) we store on 15 (birth)
-- which unit a player occupies. if player
-- then levaes and dead has a mismatch, we resolve
-- by not calling dead.
-- works if eject does not call player left unit
-- unit[unitname] = playername. if nil, no longer
-- occupied by player.
limitedAirframes.theCommand = nil
--
-- READ CONFIG ZONE TO OVERRIDE SETTING
--
function limitedAirframes.readConfigZone()
-- note: must match exactly!!!!
local theZone = cfxZones.getZoneByName("limitedAirframesConfig")
if not theZone then
trigger.action.outText("***LimA: NO config zone!", 30)
return
end
trigger.action.outText("LimA: found config zone!", 30)
-- ok, for each property, load it if it exists
if cfxZones.hasProperty(theZone, "enabled") then
limitedAirframes.enabled = cfxZones.getBoolFromZoneProperty(theZone, "enabled", true)
end
if cfxZones.hasProperty(theZone, "userCanToggle") then
limitedAirframes.userCanToggle = cfxZones.getBoolFromZoneProperty(theZone, "userCanToggle", true)
end
if cfxZones.hasProperty(theZone, "maxRed") then
limitedAirframes.maxRed = cfxZones.getNumberFromZoneProperty(theZone, "maxRed", -1)
end
if cfxZones.hasProperty(theZone, "maxBlue") then
limitedAirframes.maxBlue = cfxZones.getNumberFromZoneProperty(theZone, "maxBlue", -1)
end
if cfxZones.hasProperty(theZone, "redWinsFlag") then
limitedAirframes.redWinsFlag = cfxZones.getStringFromZoneProperty(theZone, "redWinsFlag", "999")
end
if cfxZones.hasProperty(theZone, "blueWinsFlag") then
limitedAirframes.blueWinsFlag = cfxZones.getStringFromZoneProperty(theZone, "blueWinsFlag", "998")
end
end
--
-- UNIT AND PLAYER HANDLING
--
function limitedAirframes.isKnownUnitName(uName)
if limitedAirframes.playerUnits[uName] then return true end
return false
end
function limitedAirframes.getKnownUnitPilotByUnitName(uName)
if limitedAirframes.isKnownUnitName(uName) then
return limitedAirframes.playerUnits[uName]
end
trigger.action.outText("+++lim: WARNING: " .. uName .. " is unknown!", 30)
return "***Error"
end
function limitedAirframes.getKnownUnitPilotByUnit(theUnit)
return limitedAirframes.getKnownUnitPilotByUnitName(theUnit:getName())
end
-- addPlayerUnit adds a unit as a known player unit
-- and also adds the player if unknown
function limitedAirframes.addPlayerUnit(theUnit)
local theSide = theUnit:getCoalition()
local uName = theUnit:getName()
if not uName then uName = "**XXXX**" end
local pName = theUnit:getPlayerName()
if not pName then pName = "**????**" end
limitedAirframes.updatePlayer(pName, "alive")
local desc = "unit <" .. uName .. "> controlled by <" .. pName .. ">"
if not(limitedAirframes.isKnownUnitName(uName)) then
--desc = "+++lim: added ".. desc .. " to list of known player units"
else
if limitedAirframes.playerUnits[uName] == pName then
desc = "player unit <".. uName .. "> controlled by <".. limitedAirframes.playerUnits[uName].."> re-seated"
else
desc = "Updated player unit <".. uName .. "> from <".. limitedAirframes.playerUnits[uName].."> to <" .. pName ..">"
end
end
limitedAirframes.playerUnits[uName] = pName
trigger.action.outTextForCoalition(theSide, desc, 30)
end
function limitedAirframes.killPlayer(pName)
limitedAirframes.updatePlayer(pName, "dead")
--trigger.action.outText("+++lim: PILOT LOST: " .. pName .. ", NO CSAR", 30)
end
function limitedAirframes.killPlayerInUnit(theUnit)
limitedAirframes.updatePlayerInUnit(theUnit, "dead")
--trigger.action.outText("+++lim: PILOT LOST, NO CSAR", 30)
end
function limitedAirframes.updatePlayerInUnit(theUnit, status)
local uName = theUnit:getName()
if not limitedAirframes.isKnownUnitName(uName) then
trigger.action.outText("+++lim: WARNING: updatePlayerInUnit to " .. status .. " with unknown pilot for plane", 30)
return
end
local pName = limitedAirframes.getKnownUnitPilotByUnitName(uName)
limitedAirframes.updatePlayer(pName, status)
end
function limitedAirframes.updatePlayer(pName, status)
if not pName then
trigger.action.outText("+++lim: WARNING - NIL pName in updatePlayer for status " .. status, 30)
return
end
local desc = ""
if not limitedAirframes.players[pName] then
desc = "+++lim: NEW player " .. pName .. ": " .. status
else
if limitedAirframes.players[pName] ~= status then
desc = "+++lim: CHANGE player " .. pName .. " " .. limitedAirframes.players[pName] .. " -> " .. status
else
desc = "+++: player " .. pName .. " no change (" .. status .. ")"
end
end
limitedAirframes.players[pName] = status
-- if desc then trigger.action.outText(desc, 30) end
end
function limitedAirframes.getStatusOfPlayerInUnit(theUnit)
local uName = theUnit:getName()
if not limitedAirframes.isKnownUnitName(uName) then
trigger.action.outText("+++lim: WARNING get player status for unknown pilot in plane " .. uName, 30)
return nil
end
local pName = limitedAirframes.getKnownUnitPilotByUnitName(uName)
return limitedAirframes.getStatusOfPlayerNamed(pName)
end
function limitedAirframes.getStatusOfPlayerNamed(pName)
return limitedAirframes.players[pName]
end
--
-- E V E N T H A N D L I N G
--
function limitedAirframes.XXXisKnownPlayerUnit(theUnit)
if not theUnit then return false end
local aName = theUnit:getName()
if limitedAirframes.playerUnitNames[aName] ~= nil then
return true
end
return false
end
function limitedAirframes.isInteresting(eventID)
-- return true if we are interested in this event, false else
for key, evType in pairs(limitedAirframes.myEvents) do
if evType == eventID then return true end
end
return false
end
function limitedAirframes.preProcessor(event)
-- make sure it has an initiator
if not event.initiator then return false end -- no initiator
local theUnit = event.initiator
local uName = theUnit:getName()
if event.id == 6 then -- Eject, plane already divorced from player
if limitedAirframes.isKnownUnitName(uName) then
--trigger.action.outText("limAir: detected EJECT for player unit " .. uName .. " player " .. limitedAirframes.getKnownUnitPilotByUnitName(uName), 30)
return true
end
return false -- no longer of interest
end
if event.id == 5 then -- crash, plane no longer attached to player
if limitedAirframes.isKnownUnitName(uName) then
--trigger.action.outText("limAir: detected CRASH for player unit " .. uName .. " player " .. limitedAirframes.getKnownUnitPilotByUnitName(uName), 30)
return true
end
return false -- no longer of interest
end
if not cfxPlayer.isPlayerUnit(theUnit) then
-- not a player unit. Events 5 and 6 have been
-- handled before, so we can safely ignore
return false
end
-- exclude all ground units
local theGroup = theUnit:getGroup()
local cat = theGroup:getCategory()
if cat == Group.Category.GROUND then
return false
end
-- only return true if defined as interesting
return limitedAirframes.isInteresting(event.id)
end
function limitedAirframes.postProcessor(event)
-- don't do anything
end
function limitedAirframes.somethingHappened(event)
-- when this is invoked, the preprocessor guarantees that
-- we have:
-- * an interesting event
-- * unit is valid and was a player's unit
-- the events that are relevant for pilot loss are:
-- * player entered: set pilot 'alive' state, maybe add new
-- unit and pilot to db of players and units
-- * pilot died - decrease pilot count, set 'dead' state
-- * eject - decrease pilot count, 'MIA' state, csar possible
-- * player left - when pilot status 'alive' - check safe zone
-- - outside safe zone, csar possible, set 'MIA'
-- - when pilot anything but 'alive' - ignore
if not event.initiator then
trigger.action.outText("limAir: ***WARNING: event (" .. ID .. "): no initiator, should not have been procced", 30)
return
end
local theUnit = event.initiator
local unitName = theUnit:getName()
local ID = event.id
local myType = theUnit:getTypeName()
-- "20" event (player enter): always processed
--[[-- if ID == 20 or ID == 15 then -- player entered unit
limitedAirframes.addPlayerUnit(theUnit) -- will also update player and player status to 'alive'
-- now procc a 'cheater' since we entered a new airframe/pilot
limitedAirframes.checkPlayerFrameAvailability(event)
return
end
--]]--
if ID == 20 then -- 20 ENTER UNIT
local pName = limitedAirframes.getKnownUnitPilotByUnit(theUnit)
if not pName then pName = "***UNKNOWN***" end
--trigger.action.outText("limAir: Received ENTER UNIT (20) for " .. pName .. " in " .. unitName , 30)
return
end
if ID == 15 then -- birth - this one is called in network, 20 is too unreliable
-- can set where birthed: runway, parking, air etc.
limitedAirframes.addPlayerUnit(theUnit) -- will also update player and player status to 'alive'
-- now procc a 'cheater' since we entered a new airframe/pilot
limitedAirframes.checkPlayerFrameAvailability(event)
local playerName = theUnit:getPlayerName()
limitedAirframes.unitFlownByPlayer[unitName] = playerName
-- TODO: make sure this is the ONLY plane the player
-- is registered under, and mark mismatches
trigger.action.outText("limAir: 15 -- player " .. playerName .. " now in " .. unitName, 30)
return
end
-- make sure unit's player pilot is known
if not limitedAirframes.getKnownUnitPilotByUnitName(unitName) then
trigger.action.outText("limAir: ***WARNING: Ignored player event (" .. ID .. "): unable to retrieve player name for " .. unitName, 30)
return -- plane no longer of interest cant retrieve pilot -- BUG!!!
end
-- event 6 - eject - plane divorced but player pilot is known
if ID == 6 then -- eject
limitedAirframes.pilotEjected(event)
return
end
-- event 5 - crash - plane divorced but player pilot is known
-- if pilot is still alive, this should now cause a pilot lost event if we are a helicopter
if ID == 5 then -- crash
-- as of new processing, no longer relevant
-- limitedAirframes.airFrameCrashed(event)
-- helicopters do not call died when helo
-- crashes. so check if we are still seated=alive
-- and call pilot dead then
-- for some reason, pilot died also may not be called
-- so if pilot is still alive and not MIA, he's now dead.
-- forget the helo check, this now applies to all
local pStatus = limitedAirframes.getStatusOfPlayerInUnit(theUnit)
if pStatus == "alive" then
-- this frame was carrrying a live player
limitedAirframes.pilotDied(theUnit)
return
else
trigger.action.outText("limAir: Crash of airframe detected - but player status wasn't alive (" .. pStatus .. ")", 30)
return
end
end
-- removed dual 21 detection here
if ID == 21 then
-- remove pilot name from unit name
limitedAirframes.unitFlownByPlayer[unitName] = nil
--trigger.action.outText("limAir: 21 -- unit " .. unitName .. " unoccupied", 30)
trigger.action.outText("limAir: 21 (player left) for unit " .. unitName , 30)
-- player left unit. Happens twice
-- check if player alive, else we have a ditch.
limitedAirframes.handlePlayerLeftUnit(event)
return
end
if ID == 9 then -- died
--trigger.action.outText("limAir: 9 (PILOT DEAD) for unit " .. unitName , 30)
local thePilot = limitedAirframes.unitFlownByPlayer[unitName]
if not thePilot then
trigger.action.outText("+++limAir: 9 O'RIDE -- unit " .. unitName .. " was legally vacated before!", 30)
return
end
limitedAirframes.pilotDied(theUnit)
return
end
if ID == 30 then -- unit lost
return
end
trigger.action.outText("limAir: WARNING unhandled: " .. ID .. " for player unit " .. theUnit:getName() .. " of type " .. myType, 30)
end
--
-- HANDLE VARIOUS SITUATIONS
--
function limitedAirframes.handlePlayerLeftUnit(event)
local theUnit = event.initiator
-- make sure the pilot is alive
if limitedAirframes.getStatusOfPlayerInUnit(theUnit) ~= "alive" then
-- was already handled. simply exit
local pName = limitedAirframes.getKnownUnitPilotByUnitName(theUnit:getName())
local pStatus = limitedAirframes.getStatusOfPlayerInUnit(theUnit)
-- player was already dead and has been accounted for
--trigger.action.outText("limAir: Change Plane for player <" .. pName .. "> with status <" .. pStatus .. "> procced.", 30)
return
end
-- check if the unit was inside a safe zone
-- if so, graceful exit
local uPos = theUnit:getPoint()
local meInside = cfxZones.getZonesContainingPoint(uPos, limitedAirframes.safeZones)
local mySide = theUnit:getCoalition()
--local speed = dcsCommon.getUnitSpeed(theUnit) -- this can cause problems with carriers, so check if below
--local agl = dcsCommon.getUnitAGL(theUnit) -- this will cause problems with FARP and carriers.
-- we now check the inAir
local isInAir = theUnit:inAir()
--trigger.action.outTextForCoalition(mySide, "limAir: safe check for Pilot " .. theUnit:getPlayerName() .. ": agl=" .. agl .. ", speed = " .. speed .. ", air status = " .. dcsCommon.bool2YesNo(isInAir), 30)
for i=1, #meInside do
-- I'm inside all these zones. We look for the first
-- that saves me
local theSafeZone = meInside[i]
local isSafe = false
if mySide == 1 then
isSafe = theSafeZone.redSafe
elseif mySide == 2 then
isSafe = theSafeZone.blueSafe
else
isSafe = true
end
if theSafeZone.owner then
-- owned zone. olny allow in neutral or owned by same side
isSafe = isSafe and (mySide == theSafeZone.owner or theSafeZone.owner == 0)
trigger.action.outText("+++: Lim - " .. theSafeZone.name .. " ownership: myside = " .. mySide .. " zone owner is " .. theSafeZone.owner, 30)
else
-- trigger.action.outText("+++: Zone " .. theSafeZone.name .. " has no ownership, skipping check", 30)
end
-- check we are at rest below 10m height. agl may give
-- misreadings on carriers and FARPs, while speed may read
-- wrongly on carriers (tbd). we now use isInAir to determine if
-- we ditched while in flight
--if speed > 2 or agl > 10 then isSafe = false end
--if not isInAir then return false end -- why *not* isInAir???
-- for matter of fact, why did we return ANYTHING???
-- there may be a bug here
-- maybe it should be "if isInAir then isSafe = false"??
if isInAir then isSafe = false end
if isSafe then
trigger.action.outTextForCoalition(mySide, "limAir: Pilot " .. theUnit:getPlayerName() .. " left unit " .. theUnit:getName() .. " legally in zone " .. theSafeZone.name, 30)
-- remove from known player planes
-- no more limitedAirframes.removePlayerUnit(theUnit)
return;
end
end
-- ditched outside safe harbour
trigger.action.outTextForCoalition(mySide, "Pilot " .. theUnit:getPlayerName() .. " DITCHED unit " .. theUnit:getName() .. " -- PILOT LOSS (MIA)", 30)
limitedAirframes.pilotLost(theUnit)
if csarManager and csarManager.airframeDitched then
csarManager.airframeDitched(theUnit)
end
limitedAirframes.updatePlayerInUnit(theUnit, "MIA") -- cosmetic only
limitedAirframes.createCSAR(theUnit)
end
function limitedAirframes.pilotEjected(event)
local theUnit = event.initiator
-- do we want to check location?
-- no. if the user ejects, plane is done for
local theSide = theUnit:getCoalition()
local pilot = limitedAirframes.getKnownUnitPilotByUnit(theUnit)
local uName = theUnit:getName()
trigger.action.outTextForCoalition(theSide, "Pilot <" .. pilot .. "> ejected from " .. uName .. ", now MIA", 30)
local hasLostTheWar = limitedAirframes.pilotLost(theUnit)
limitedAirframes.updatePlayerInUnit(theUnit, "MIA") -- cosmetic only
-- create CSAR if applicable
if not hasLostTheWar then
limitedAirframes.createCSAR(theUnit)
end
end
function limitedAirframes.pilotDied(theUnit)
--limitedAirframes.killPlayerInUnit(theUnit)
local theSide = theUnit:getCoalition()
local pilot = limitedAirframes.getKnownUnitPilotByUnit(theUnit)
local uName = theUnit:getName()
trigger.action.outTextForCoalition(theSide, "Pilot <" .. pilot .. "> is confirmed KIA while controlling " .. uName, 30)
limitedAirframes.pilotLost(theUnit)
end
function limitedAirframes.pilotLost(theUnit)
-- returns true if lost the war
-- MUST NOT MESSAGE PILOT STATUS AS MIA CAN ALSO BE SET
-- first DELETE THE UNIT FROM player-owned unit table
-- so an empty crash after eject/death will not be counted as two losses
limitedAirframes.killPlayerInUnit(theUnit)
-- now see if we are enabled to limit airframes
if not limitedAirframes.enabled then return false end
-- find out which side lost the airframe and message side
local theSide = theUnit:getCoalition()
local pilot = limitedAirframes.getKnownUnitPilotByUnit(theUnit)
local uName = theUnit:getName()
--trigger.action.outTextForCoalition(theSide, "Pilot <" .. pilot .. "> is confirmed KIA while controlling " .. uName, 30)
if theSide == 1 then -- red
theOtherSide = 2
if limitedAirframes.maxRed < 0 then return false end -- disabled/infinite
limitedAirframes.currRed = limitedAirframes.currRed - 1
if limitedAirframes.currRed == 0 then
trigger.action.outTextForCoalition(theSide, "\nYou have lost almost all of your pilots.\n\nWARNING: Losing any more pilots WILL FAIL THE MISSION\n", 30)
trigger.action.outSoundForCoalition(theSide, "Quest Snare 3.wav")
return false
end
if limitedAirframes.currRed < 0 then
-- red have lost all airframes
trigger.action.outText("\nREDFORCE has lost all of their pilots.\n\nBLUEFORCE WINS!\n", 30)
trigger.action.outSoundForCoalition(theSide, "Death PIANO.wav")
trigger.action.outSoundForCoalition(theOtherSide, "Triumphant Victory.wav")
trigger.action.setUserFlag(limitedAirframes.blueWinsFlag, 1 )
return true
end
elseif theSide == 2 then -- blue
theOtherSide = 1
if limitedAirframes.maxBlue < 0 then return false end -- disabled/infinite
limitedAirframes.currBlue = limitedAirframes.currBlue - 1
if limitedAirframes.currBlue == 0 then
trigger.action.outTextForCoalition(theSide, "\nYou have lost almost all of your pilots.\n\nWARNING: Losing any more pilots WILL FAIL THE MISSION\n", 30)
trigger.action.outSoundForCoalition(theSide, "Quest Snare 3.wav")
return false
end
if limitedAirframes.currBlue < 0 then
-- red have lost all airframes
trigger.action.outText("\nBLUEFORCE has lost all of their pilots.\n\nREDFORCE WINS!\n", 30)
trigger.action.setUserFlag(limitedAirframes.redWinsFlag, 1 )
trigger.action.outSoundForCoalition(theSide, "Death PIANO.wav")
trigger.action.outSoundForCoalition(theOtherSide, "Triumphant Victory.wav")
return true
end
trigger.action.outSoundForCoalition(theSide, "Quest Snare 3.wav")
trigger.action.outTextForCoalition(theSide, "You have lost a pilot! Remaining: " .. limitedAirframes.currBlue, 30)
end
return false
end
function limitedAirframes.checkPlayerFrameAvailability(event)
local theUnit = event.initiator
local theSide = theUnit:getCoalition()
if theSide == 1 then -- red
if limitedAirframes.maxRed < 0 then return end -- disabled/infinite
if limitedAirframes.currRed < 0 then
-- red have lost all airframes
trigger.action.outText("\nREDFORCE is a CHEATER!\n", 30)
return
end
elseif theSide == 2 then -- blue
if limitedAirframes.maxBlue < 0 then return end -- disabled/infinite
if limitedAirframes.currBlue < 0 then
-- red have lost all airframes
trigger.action.outText("\nBLUEFORCE is a CHEATER!\n", 30)
return
end
end
end
function limitedAirframes.createCSAR(theUnit)
-- only do this if we have installed CSAR Manager
if csarManager and csarManager.createCSARforUnit then
csarManager.createCSARforUnit(theUnit,
limitedAirframes.getKnownUnitPilotByUnit(theUnit),
100)
end
end
-- start up
function limitedAirframes.addSafeZone(aZone)
if not aZone then
trigger.action.outText("WARNING: NIL Zone in addSafeZone", 30)
return
end
-- transfer properties if they exist
-- blueSafe, redSafe: what side this is safe for, default = yes
-- add zone to my list
limitedAirframes.safeZones[aZone] = aZone
aZone.redSafe = true
aZone.redSafe = cfxZones.getBoolFromZoneProperty(aZone, "redSafe", true)
aZone.blueSafe = true
aZone.blueSafe = cfxZones.getBoolFromZoneProperty(aZone, "blueSafe", true)
trigger.action.outText("limAir: added safeZone " .. aZone.name, 30)
end
--
-- COMMAND & CONFIGURATION
--
function limitedAirframes.setCommsMenu()
local desc = "Pilot Count (Currently ON)"
local desc2 = "Turn OFF Pilot Count (Cheat)?"
if not limitedAirframes.enabled then
desc = "Pilot Count (Currently OFF)"
desc2 = "ENABLE Pilot Count"
end
-- remove previous version
if limitedAirframes.rootMenu then
missionCommands.removeItem(limitedAirframes.theScore)
missionCommands.removeItem(limitedAirframes.theCommand)
missionCommands.removeItem(limitedAirframes.rootMenu)
end
limitedAirframes.theCommand = nil
limitedAirframes.rootMenu = nil
-- add current version menu and command
limitedAirframes.rootMenu = missionCommands.addSubMenu(desc, nil)
limitedAirframes.theScore = missionCommands.addCommand("How many airframes left?" , limitedAirframes.rootMenu, limitedAirframes.redirectAirframeScore, {"none"})
limitedAirframes.theCommand = missionCommands.addCommand(desc2 , limitedAirframes.rootMenu, limitedAirframes.redirectToggleAirFrames, {"none"})
end
function limitedAirframes.redirectAirframeScore(args)
timer.scheduleFunction(limitedAirframes.doAirframeScore, args, timer.getTime() + 0.1)
end
function limitedAirframes.doAirframeScore(args)
local redRemaining = "unlimited"
if limitedAirframes.maxRed >= 0 then
redRemaining = limitedAirframes.currRed .. " of " .. limitedAirframes.maxRed
if limitedAirframes.currRed < 1 then
redRemaining = "no"
end
end
local blueRemaining = "unlimited"
if limitedAirframes.maxBlue >= 0 then
blueRemaining = limitedAirframes.currBlue .. " of " .. limitedAirframes.maxBlue
if limitedAirframes.currBlue < 1 then
blueRemaining = "no"
end
end
local msg = "\nRED has " .. redRemaining .. " pilots left,\nBLUE has " .. blueRemaining .. " pilots left\n"
trigger.action.outText(msg, 30, true)
trigger.action.outSound("Quest Snare 3.wav")
end
function limitedAirframes.redirectToggleAirFrames(args)
timer.scheduleFunction(limitedAirframes.doToggleAirFrames, args, timer.getTime() + 0.1)
end
function limitedAirframes.doToggleAirFrames(args)
limitedAirframes.enabled = not limitedAirframes.enabled
limitedAirframes.setCommsMenu()
local desc = "\n\nPilot Count rule NOW IN EFFECT\n\n"
if limitedAirframes.enabled then
trigger.action.outSound("Quest Snare 3.wav")
else
desc = "\n\nYou cowardly disabled Pilot Count\n\n"
trigger.action.outSound("Death PIANO.wav")
end
trigger.action.outText(desc, 30)
limitedAirframes.setCommsMenu()
end
--
-- CSAR CALLBACK
--
function limitedAirframes.pilotsRescued(theCoalition, success, numRescued, notes)
local availablePilots = 0
if theCoalition == 1 then -- red
limitedAirframes.currRed = limitedAirframes.currRed + numRescued
if limitedAirframes.currRed > limitedAirframes.maxRed then
limitedAirframes.currRed = limitedAirframes.maxRed
end
availablePilots = limitedAirframes.currRed
if limitedAirframes.maxRed < 0 then
availablePilots = "unlimited"
end
end
if theCoalition == 2 then -- blue
limitedAirframes.currBlue = limitedAirframes.currBlue + numRescued
if limitedAirframes.currBlue > limitedAirframes.maxBlue then
limitedAirframes.currBlue = limitedAirframes.maxBlue
end
availablePilots = limitedAirframes.currBlue
if limitedAirframes.maxBlue < 0 then
availablePilots = "unlimited"
end
end
trigger.action.outTextForCoalition(theCoalition, "\nPilots returned to flight line, you now have " .. availablePilots..".\n", 30)
trigger.action.outSoundForCoalition(theCoalition, "Quest Snare 3.wav")
end
--
-- START
--
function limitedAirframes.start()
if not dcsCommon.libCheck("cfx Limited Airframes",
limitedAirframes.requiredLibs) then
return false
end
-- override config settings if defined as zone
limitedAirframes.readConfigZone()
-- collect all zones that are airframe safe
local afsZones = cfxZones.zonesWithProperty("pilotSafe")
-- now add all zones to my zones table, and init additional info
-- from properties
for k, aZone in pairs(afsZones) do
limitedAirframes.addSafeZone(aZone)
end
-- connect player callback
-- install callbacks for airframe-related events
dcsCommon.addEventHandler(limitedAirframes.somethingHappened, limitedAirframes.preProcessor, limitedAirframes.postProcessor)
-- set current values
limitedAirframes.currRed = limitedAirframes.maxRed
limitedAirframes.currBlue = limitedAirframes.maxBlue
-- collect active player unit names
local allPlayerUnits = cfxPlayer.getAllExistingPlayerUnitsRaw()
for i=1, #allPlayerUnits do
local aUnit = allPlayerUnits[i]
limitedAirframes.addPlayerUnit(aUnit)
-- trigger.action.outText("limAir: detected active player unit " .. aUnit:getName(), 30)
end
-- allow configuration menu
if limitedAirframes.userCanToggle then
limitedAirframes.setCommsMenu()
end
-- connect to csarManager if present
if csarManager and csarManager.installCallback then
csarManager.installCallback(limitedAirframes.pilotsRescued)
trigger.action.outText("+++lim: connected to csar manager", 30)
else
trigger.action.outText("+++lim: NO CSAR integration", 30)
end
-- say hi
trigger.action.outText("limitedAirframes v" .. limitedAirframes.version .. " started: R:".. limitedAirframes.maxRed .. "/B:" .. limitedAirframes.maxBlue, 30)
return true
end
if not limitedAirframes.start() then
limitedAirframes = nil
trigger.action.outText("cf/x Limited Airframes aborted: missing libraries", 30)
end
--[[--
safe ditch: check airspeed and altitude. ditch only counts if less than 10m and 2 kts
report number of airframes left via second instance in switch off menu
--]]--

218
modules/nameStats.lua Normal file
View File

@ -0,0 +1,218 @@
nameStats = {}
nameStats.version = "1.1.0"
--[[--
package that allows generic and global access to data, stored by
name and path. Can be used to manage score, cargo, statistics etc.
provides its own root by default, but modules can provide their own
private roots to be managed the same way.
Uses a path metaphor to access fine grained levels
version history
1.0.0 - initial release
1.1.0 - added table to leaf for more general
- added ability to use arbitrary roots
- setValue
- reset
- getAllPathes
1.1.1 - simplified strings to a single string
- left old logic intact for log extensions
- setString()
--]]--
-- statistics container. everything is in here
nameStats.stats = {}
--[[--
to access data, there are the following principles:
- name: uniquely defines a branch where all data for this name
is stored. MANDATORY, MUST NEVER BE nil
this is usually a unit's name
- path: in each branch you can have a path (string) to the data
to more precisely define. for example, you can have separate
paths 'score' and 'weight' under the same name
There currently is no logic attached to a path except that
it must be unique inside the same branch
OPTIONAL. if omittted, a default data set is returned
- rootNode: OPTIONAL storage (table) you can pass to create your own
pricate storage space that can't be accessed unless the
invoking method also passes the same rootNode
Data
Data is stored in a "leaf node" that has three properties
- value: a numerical value that can be set and changed
- string: a strings that can be set and added to
- table: a table that you can treat as you like and that
is never looked into nor changed by this module
--]]--
function nameStats.getAllNames(theRootNode) -- root node is optional
if not theRootNode then theRootNode = nameStats.stats end
local allNames = {}
for name, entry in pairs(theRootNode) do
table.insert(allNames, name)
end
return allNames
end
function nameStats.getAllPathes(name, theRootNode)
if not theRootNode then theRootNode = nameStats.stats end
local allPathes = {}
local theEntry = theRootNode[name]
if not theEntry then
return allPathes
end
for pathName, data in pairs(theEntry.data) do
table.insert(allPathes, pathName)
end
return allPathes
end
-- change the numerical value by delta. use negative numbers to decrease
function nameStats.changeValue(name, delta, path, rootNode)
if not name then return nil end
local theLeaf = nameStats.getLeaf(name, path, rootNode)
theLeaf.value = theLeaf.value + delta
return theLeaf.value
end
-- set to a specific value
function nameStats.setValue(name, newVal, path, rootNode)
if not name then return nil end
local theLeaf = nameStats.getLeaf(name, path, rootNode)
theLeaf.value = newVal
return theLeaf.value
end
-- add a string to the log
function nameStats.addString(name, aString, path, rootNode)
if not name then return nil end
if not aString then return nil end
local theLeaf = nameStats.getLeaf(name, path, rootNode)
--table.insert(theLeaf.strings, aString)
theLeaf.strings = theLeaf.strings .. aString
-- return aString
end
-- reset the log
function nameStats.removeAllString(name, path, rootNode)
if not name then return nil end
local theLeaf = nameStats.getLeaf(name, path, rootNode)
-- theLeaf.strings = {}
theLeaf.strings = ""
end
function nameStats.setString(name, aString, path, rootNode)
if not name then return nil end
local theLeaf = nameStats.getLeaf(name, path, rootNode)
theLeaf.strings = aString
end
-- set the table variable
function nameStats.setTable(name, path, aTable, rootNode)
if not name then return end
local theLeaf = nameStats.getLeaf(name, path, rootNode)
theLeaf.theTable = aTable
end
-- get the numerical value associated with name, path
function nameStats.getValue(name, path, rootNode) -- allocate if not exist
if not name then return nil end
local theLeaf = nameStats.getLeaf(name, path, rootNode)
return theLeaf.value
end
-- get the log associated with name, path
function nameStats.getStrings(name, path, rootNode)
if not name then return nil end
local theLeaf = nameStats.getLeaf(name, path, rootNode)
return theLeaf.strings
end
-- alias for compatibility reasons
function nameStats.getString(name, path, rootNode)
return nameStats.getStrings(name, path, rootNode)
end
-- get the table stored under name, path.
function nameStats.getTable(name, path, rootNode)
if not name then return nil end
local theLeaf = nameStats.getLeaf(name, path, rootNode)
return theLeaf.theTable
end
-- reset whatever is stored under name, path
-- WARNING: passing nil path will entirely reset the whole name
function nameStats.reset(name, path, rootNode)
if not name then return nil end
if not rootNode then rootNode = nameStats.stats end
local theEntry = rootNode[name]
if not theEntry then
-- does not yet exist, create a root entry
theEntry = nameStats.createRoot(name)
rootNode[name] = theEntry
nameStats.getLeaf(name, path, rootNode) -- will alloc an empty leaf
return -- done
end
if not path then -- will delete everything!!!
theEntry = nameStats.createRoot(name)
rootNode[name] = theEntry
return
end
-- create new leaf and replace existing
theLeaf = nameStats.createLeaf()
theEntry.data[path] = theLeaf
end
--
--
-- private function
--
--
function nameStats.getLeaf(name, path, rootNode)
if not name then return nil end
if not rootNode then rootNode = nameStats.stats end
-- will allocate if not existlocal theEntry = nameStats.stats[name]
local theEntry = rootNode[name]
if not theEntry then
-- does not yet exist, create a root entry
theEntry = nameStats.createRoot(name)
rootNode[name] = theEntry
end
-- from here on, the entry exists
if not path then return theEntry.defaultLeaf end
-- access via path
local theLeaf = theEntry.data[path]
if not theLeaf then
theLeaf = nameStats.createLeaf()
theEntry.data[path] = theLeaf
end
return theLeaf
end
function nameStats.createLeaf()
local theLeaf = {}
theLeaf.value = 0
theLeaf.strings = ""
theLeaf.log = {} -- was strings
theLeaf.theTable = {}
return theLeaf
end
-- for each entry in stats, this is the root container
function nameStats.createRoot(name)
local theRoot = {} -- all nodes are in here
theRoot.name = name
theRoot.data = {} -- dict by path for leafs
theRoot.defaultLeaf = nameStats.createLeaf()
return theRoot
end
-- say hi!
trigger.action.outText("cf/x NameStats v" .. nameStats.version .. " loaded", 30)

36
modules/parashoo.lua Normal file
View File

@ -0,0 +1,36 @@
parashoo = {}
parashoo.version = "1.1.0"
--[[--
VERSION HISTORY
- 1.0.0 initial version
- 1.1.0 wait 3 minutes before destroying para
guy, else KIA reported when player still
in pilot
--]]--
parashoo.killDelay = 3 * 60 -- 3 minutes delay
function parashoo.removeGuy(args)
local theGuy = args.theGuy
if theGuy and theGuy:isExist() then
Unit.destroy(theGuy)
end
end
-- remove parachuted pilots after landing
function parashoo:onEvent(event)
if event.id == 31 then -- landing_after_eject
if event.initiator then
local args = {}
args.theGuy = event.initiator
timer.scheduleFunction(parashoo.removeGuy, args, timer.getTime() + parashoo.killDelay)
--Unit.destroy(event.initiator) -- old direct remove
end
end
end
-- add event handler
world.addEventHandler(parashoo)
trigger.action.outText("parashoo v" .. parashoo.version .. " loaded.", 30)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,97 @@
dmlMain = {}
--
-- DML-based mission skeleton with event loop
--
dmlMain.ups = 1 -- 1 update per second
-- minimal libraries required
dmlMain.requiredLibs = {
"dcsCommon", -- minimal module for all
"cfxZones", -- Zones, of course
"cfxPlayer", -- player events
}
dmlMain.config = {} -- for reading config zones
--
-- world event handling
--
function dmlMain.wPreProc(event)
return true -- true means invoke worldEventHanlder()
-- filter here and return false if the event is to be ignored
end
function dmlMain.worldEventHandler(event)
-- now analyse table <event> and do stuff
trigger.action.outText("DCS World Event " .. event.id .. " (" .. dcsCommon.event2text(event.id) .. ") received", 30)
end
--
-- player event handling
--
function dmlMain.playerEventHandler (evType, description, info, data)
trigger.action.outText("DML Player Event " .. evType .. " received", 30)
end
--
-- update loop
--
function dmlMain.update()
-- schedule myself in 1/ups seconds
timer.scheduleFunction(dmlMain.update, {}, timer.getTime() + 1/dmlMain.ups)
-- perform any regular checks here in your main loop
end
--
-- read configuration from zone 'dmlMainConfig'
--
function dmlMain.readConfiguration()
local theZone = cfxZones.getZoneByName("dmlMainConfig")
if not theZone then return end
dmlMain.config = cfxZones.getAllZoneProperties(theZone)
-- demo: dump all name/value pairs returned
trigger.action.outText("DML config read from config zone:", 30)
for name, value in pairs(dmlMain.config) do
trigger.action.outText(name .. ":" .. value, 30)
end
trigger.action.outText("---- (end of list)", 30)
end
--
-- start
--
function dmlMain.start()
-- ensure that all modules have loaded
if not dcsCommon.libCheck("DML Main",
dmlMain.requiredLibs) then
return false
end
-- read any configuration values placed in a config zone on the map
dmlMain.readConfiguration()
-- subscribe to world events
dcsCommon.addEventHandler(dmlMain.worldEventHandler,
dmlMain.wPreProc) -- no post nor rejected
-- subscribe to player events
cfxPlayer.addMonitor(dmlMain.playerEventHandler)
-- start the event loop. it will sustain itself
dmlMain.update()
-- say hi!
trigger.action.outText("DML Main mission running!", 30)
return true
end
-- start main
if not dmlMain.start() then
trigger.action.outText("Main mission failed to run", 30)
dmlMain = nil
end

View File

@ -0,0 +1,104 @@
ldgCtr = {}
--
-- DML-based mission that counts the number of
-- successful missions for all players. MP-capable
--
ldgCtr.ups = 1 -- 1 update per second
-- minimal libraries required
ldgCtr.requiredLibs = {
"dcsCommon", -- minimal module for all
"cfxZones", -- Zones, of course
"cfxPlayer", -- player events
}
ldgCtr.config = {} -- for reading config zones
ldgCtr.landings = {} -- set all landings to 0
--
-- world event handling
--
function ldgCtr.wPreProc(event)
return event.id == 4 -- look for 'landing event'
end
function ldgCtr.worldEventHandler(event)
-- wPreProc filters all events EXCEPT landing
local theUnit = event.initiator
local uName = theUnit:getName()
local playerName = theUnit:getPlayerName()
trigger.action.outText(uName .. " has landed.", 30)
if playerName then
-- if a player landed, count their landing
local numLandings = ldgCtr.landings[playerName]
if not numLandings then numLandings = 0 end
numLandings = numLandings + 1
ldgCtr.landings[playerName] = numLandings
trigger.action.outText("Player " .. playerName .. " completed ".. numLandings .." landings.", 30)
end
end
--
-- player event handling
--
function ldgCtr.playerEventHandler (evType, description, info, data)
-- not needed
end
--
-- update loop
--
function ldgCtr.update()
-- schedule myself in 1/ups seconds
timer.scheduleFunction(ldgCtr.update, {}, timer.getTime() + 1/ldgCtr.ups)
-- no regular checks needed
end
--
-- read configuration from zone 'ldgCtrConfig'
--
function ldgCtr.readConfiguration()
local theZone = cfxZones.getZoneByName("ldgCtrConfig")
if not theZone then return end
ldgCtr.config = cfxZones.getAllZoneProperties(theZone)
end
--
-- start
--
function ldgCtr.start()
-- ensure that all modules have loaded
if not dcsCommon.libCheck("Landing Counter",
ldgCtr.requiredLibs) then
return false
end
-- read any configuration values placed in a config zone on the map
ldgCtr.readConfiguration()
-- init variables & state
ldgCtr.landings = {}
-- subscribe to world events
dcsCommon.addEventHandler(ldgCtr.worldEventHandler,
ldgCtr.wPreProc) -- no post nor rejected
-- subscribe to player events
cfxPlayer.addMonitor(ldgCtr.playerEventHandler)
-- start the event loop. it will sustain itself
ldgCtr.update()
-- say hi!
trigger.action.outText("Landing Counter mission running!", 30)
return true
end
-- start main
if not ldgCtr.start() then
trigger.action.outText("Landing Counter failed to run", 30)
ldgCtr = nil
end