mirror of
https://github.com/weyne85/DML.git
synced 2025-10-29 16:57:49 +00:00
Initial commit
This commit is contained in:
commit
929a188497
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
BIN
Doc/DML Documentation.pdf
Normal file
BIN
Doc/DML Documentation.pdf
Normal file
Binary file not shown.
455
modules/FARPZones.lua
Normal file
455
modules/FARPZones.lua
Normal 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
203
modules/cargoSuper.lua
Normal 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
406
modules/cfxArtillery.lua
Normal 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
714
modules/cfxArtilleryUI.lua
Normal 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
|
||||
--]]--
|
||||
448
modules/cfxArtilleryZones.lua
Normal file
448
modules/cfxArtilleryZones.lua
Normal 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
171
modules/cfxCargoManager.lua
Normal 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
|
||||
256
modules/cfxCargoReceiver.lua
Normal file
256
modules/cfxCargoReceiver.lua
Normal 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
515
modules/cfxCommander.lua
Normal 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
1138
modules/cfxGroudTroops.lua
Normal file
File diff suppressed because it is too large
Load Diff
158
modules/cfxGroups.lua
Normal file
158
modules/cfxGroups.lua
Normal 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
826
modules/cfxHeloTroops.lua
Normal 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
99
modules/cfxMapMarkers.lua
Normal 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
126
modules/cfxNDB.lua
Normal 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
|
||||
150
modules/cfxObjectDestructDetector.lua
Normal file
150
modules/cfxObjectDestructDetector.lua
Normal 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
|
||||
481
modules/cfxObjectSpawnZones.lua
Normal file
481
modules/cfxObjectSpawnZones.lua
Normal 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
860
modules/cfxOwnedZones.lua
Normal 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
644
modules/cfxPlayer.lua
Normal 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
350
modules/cfxPlayerScore.lua
Normal 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
|
||||
|
||||
303
modules/cfxPlayerScoreUI.lua
Normal file
303
modules/cfxPlayerScoreUI.lua
Normal 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
385
modules/cfxReconGUI.lua
Normal 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
606
modules/cfxReconMode.lua
Normal 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
470
modules/cfxSSBClient.lua
Normal 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
154
modules/cfxSSBSingleUse.lua
Normal 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
111
modules/cfxSmokeZones.lua
Normal 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
485
modules/cfxSpawnZones.lua
Normal 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
1323
modules/cfxZones.lua
Normal file
File diff suppressed because it is too large
Load Diff
133
modules/cfxmon.lua
Normal file
133
modules/cfxmon.lua
Normal 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
572
modules/civAir.lua
Normal 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
1151
modules/csarManager2.lua
Normal file
File diff suppressed because it is too large
Load Diff
2063
modules/dcsCommon.lua
Normal file
2063
modules/dcsCommon.lua
Normal file
File diff suppressed because it is too large
Load Diff
515
modules/guardianAngel.lua
Normal file
515
modules/guardianAngel.lua
Normal 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
395
modules/jtacGrpUI.lua
Normal 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
|
||||
828
modules/limitedAirframes.lua
Normal file
828
modules/limitedAirframes.lua
Normal 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
218
modules/nameStats.lua
Normal 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
36
modules/parashoo.lua
Normal 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)
|
||||
BIN
tutorial & demo missions/demo - 000 smoke em! DML intro.miz
Normal file
BIN
tutorial & demo missions/demo - 000 smoke em! DML intro.miz
Normal file
Binary file not shown.
BIN
tutorial & demo missions/demo - ADF and NDB fun.miz
Normal file
BIN
tutorial & demo missions/demo - ADF and NDB fun.miz
Normal file
Binary file not shown.
BIN
tutorial & demo missions/demo - DML mission template.miz
Normal file
BIN
tutorial & demo missions/demo - DML mission template.miz
Normal file
Binary file not shown.
BIN
tutorial & demo missions/demo - Event Monitor.miz
Normal file
BIN
tutorial & demo missions/demo - Event Monitor.miz
Normal file
Binary file not shown.
BIN
tutorial & demo missions/demo - Landing Counter.miz
Normal file
BIN
tutorial & demo missions/demo - Landing Counter.miz
Normal file
Binary file not shown.
BIN
tutorial & demo missions/demo - ME triggered spawn.miz
Normal file
BIN
tutorial & demo missions/demo - ME triggered spawn.miz
Normal file
Binary file not shown.
BIN
tutorial & demo missions/demo - Owned Zones ME integration.miz
Normal file
BIN
tutorial & demo missions/demo - Owned Zones ME integration.miz
Normal file
Binary file not shown.
BIN
tutorial & demo missions/demo - artillery with UI.miz
Normal file
BIN
tutorial & demo missions/demo - artillery with UI.miz
Normal file
Binary file not shown.
BIN
tutorial & demo missions/demo - artillery zones triggered.miz
Normal file
BIN
tutorial & demo missions/demo - artillery zones triggered.miz
Normal file
Binary file not shown.
BIN
tutorial & demo missions/demo - helo cargo.miz
Normal file
BIN
tutorial & demo missions/demo - helo cargo.miz
Normal file
Binary file not shown.
BIN
tutorial & demo missions/demo - helo trooper.miz
Normal file
BIN
tutorial & demo missions/demo - helo trooper.miz
Normal file
Binary file not shown.
Binary file not shown.
BIN
tutorial & demo missions/demo - moving spawners.miz
Normal file
BIN
tutorial & demo missions/demo - moving spawners.miz
Normal file
Binary file not shown.
BIN
tutorial & demo missions/demo - object destruct detection.miz
Normal file
BIN
tutorial & demo missions/demo - object destruct detection.miz
Normal file
Binary file not shown.
BIN
tutorial & demo missions/demo - player score.miz
Normal file
BIN
tutorial & demo missions/demo - player score.miz
Normal file
Binary file not shown.
BIN
tutorial & demo missions/demo - recon mode.miz
Normal file
BIN
tutorial & demo missions/demo - recon mode.miz
Normal file
Binary file not shown.
Binary file not shown.
BIN
tutorial & demo missions/distressbeacon.ogg
Normal file
BIN
tutorial & demo missions/distressbeacon.ogg
Normal file
Binary file not shown.
97
tutorial & demo missions/dml skeleton mission code.lua
Normal file
97
tutorial & demo missions/dml skeleton mission code.lua
Normal 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
|
||||
104
tutorial & demo missions/ldgCtr.lua
Normal file
104
tutorial & demo missions/ldgCtr.lua
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user