DML/modules/impostors.lua
Christian Franz 780797aba3 Version 1.03
The Debugger
2022-07-14 08:09:21 +02:00

570 lines
18 KiB
Lua

impostors={}
impostors.version = "1.0.1"
impostors.verbose = false
impostors.ups = 1
impostors.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
"cfxMX",
}
impostors.impostorZones = {}
--impostors.impostors = {} -- these are sorted by name of the orig group and contain the units by name of the impostors
impostors.callbacks = {}
impostors.uniqueCounter = 8200000 -- clones start at 9200000
--[[--
Version History
1.0.0 - initial version
1.0.1 - added some verbosity
LIMITATIONS:
must be on ground (or would be very silly
does not work with any units deployed on ships
Positioning AI Planed (ME shortcoming): add a waypoint so it orients itself.
--]]--
--
-- adding / removing from list
--
function impostors.addImpostorZone(theZone)
table.insert(impostors.impostorZones, theZone)
end
function impostors.getCloneZoneByName(aName)
for idx, aZone in pairs(impostors.impostorZones) do
if aName == aZone.name then return aZone end
end
if impostors.verbose then
trigger.action.outText("+++ipst: no impostor with name <" .. aName ..">", 30)
end
return nil
end
--
-- spawn impostors from data
--
function impostors.uniqueID()
local uid = impostors.uniqueCounter
impostors.uniqueCounter = impostors.uniqueCounter + 1
return uid
end
function impostors.spawnImpostorsFromData(rawData, cat, ctry)
local theImpostors = {}
-- we iterate a group's raw data unit by unit and create
-- a static for each unit, named exactly as the original unit
-- modifies rawData for use later
for idx, unitData in pairs(rawData.units) do
-- build impostor record
local ir = {}
ir.heading = unitData.heading
ir.type = unitData.type
ir.name = rawData.name .. "-" .. tostring(impostors.uniqueID())
ir.groupID = impostors.uniqueID()
ir.unitId = impostors.uniqueID()
theImpostors[unitData.name] = ir.name -- for lookup later
ir.x = unitData.x
ir.y = unitData.y
ir.livery_id = unitData.livery_id
if impostors.verbose then
trigger.action.outText("+++impostoring unit <" .. unitData.name .. ">: name <" .. ir.name .. ">, x <" .. ir.x .. ">, y <" .. ir.y .. ">, heading <" .. ir.heading .. ">, type <" .. ir.type .. "> ", 30)
end
local linkedZones = unitData.linkedZones
-- spawn the impostor
local theImp = coalition.addStaticObject(ctry, ir)
-- relink linked zones to this
if #linkedZones > 0 then
for idx, theZone in pairs(linkedZones) do
theZone.linkedUnit = theImp
if theZone.verbose then
trigger.action.outText("+++ipst: imp-linked zone <" .. theZone.name .. "> to imp <" .. theImp:getName() .. ">", 30)
end
end
end
end
return theImpostors
end
--
-- read impostor zone
--
function impostors.getRawDataFromGroupNamed(gName)
if gName then
theGroup = Group.getByName(gName)
if not theGroup then
trigger.action.outText("+++ipst: getRawDataFromGroupName cant find group <" .. gName .. ">", 30)
return nil, nil, nil
end
else
trigger.action.outText("+++ipst: getRawDataFromGroupName has no name to look up", 30)
return nil, nil, nil
end
local groupName = gName
local cat = theGroup:getCategory()
-- access mxdata for livery because getDesc does not return the livery
local liveries = {}
local mxData = cfxMX.getGroupFromDCSbyName(gName)
for idx, theUnit in pairs (mxData.units) do
liveries[theUnit.name] = theUnit.livery_id
end
local ctry
local gID = theGroup:getID()
local allUnits = theGroup:getUnits()
local rawGroup = {}
rawGroup.name = groupName
local rawUnits = {}
for idx, theUnit in pairs(allUnits) do
local ir = {}
local unitData = theUnit:getDesc()
-- build record
ir.heading = dcsCommon.getUnitHeading(theUnit)
ir.name = theUnit:getName()
ir.type = unitData.typeName -- warning: fields are called differently! typename vs type
ir.livery_id = liveries[ir.name] -- getDesc does not return livery
ir.groupId = gID
ir.unitId = theUnit:getID()
local up = theUnit:getPoint()
ir.x = up.x
ir.y = up.z -- !!! warning!
-- see if any zones are linked to this unit
ir.linkedZones = cfxZones.zonesLinkedToUnit(theUnit)
table.insert(rawUnits, ir)
ctry = theUnit:getCountry()
end
rawGroup.ctry = ctry
rawGroup.cat = cat
rawGroup.units = rawUnits
return rawGroup, cat, ctry
end
function impostors.createImpostorWithZone(theZone) -- has "impostor?"
if impostors.verbose or theZone.verbose then
trigger.action.outText("+++ipst: new impostor " .. theZone.name, 30)
end
-- the impostor? is the flag. we always have it.
-- must match aZone.impostorFlag, aZone.impostorTriggerMethod, "lastImpostorValue"
theZone.impostorFlag = cfxZones.getStringFromZoneProperty(theZone, "impostor?", "*<none>")
theZone.lastImpostorValue = cfxZones.getFlagValue(theZone.impostorFlag, theZone)
if cfxZones.hasProperty(theZone, "reanimate?") then
theZone.reanimateFlag = cfxZones.getStringFromZoneProperty(theZone, "reanimate?", "*<none>")
theZone.lastReanimateValue = cfxZones.getFlagValue(theZone.reanimateFlag, theZone)
end
-- watchflag:
-- triggerMethod
theZone.impostorTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change")
if cfxZones.hasProperty(theZone, "impostorTriggerMethod") then
theZone.impostorTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "impostorTriggerMethod", "change")
end
-- local localGroups = impostors.allGroupsInZoneByData(theZone)
theZone.groupNames = cfxZones.allGroupNamesInZone(theZone)
theZone.impostor = false -- we have not yet turned units into impostors
theZone.myImpostors = {}
theZone.origin = cfxZones.getPoint(theZone) -- save reference point for all groupVectors
theZone.onStart = cfxZones.getBoolFromZoneProperty(theZone, "onStart", false)
-- blinking
theZone.blinkTime = cfxZones.getNumberFromZoneProperty(theZone, "blink", -1)
theZone.blinkCount = 0
-- interface to groupTracker tbc
if cfxZones.hasProperty(theZone, "trackWith:") then
theZone.trackWith = cfxZones.getStringFromZoneProperty(theZone, "trackWith:", "<None>")
end
-- check onStart, and act accordingly
if theZone.onStart then
impostors.turnZoneIntoImpostors(theZone)
end
-- all dead
if cfxZones.hasProperty(theZone, "allDead!") then
theZone.allDead = cfxZones.getStringFromZoneProperty(theZone, "allDead", "<None>")
end
theZone.impostorMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc")
if cfxZones.hasProperty(theZone, "impostorMethod") then
theZone.impostorMethod = cfxZones.getStringFromZoneProperty(theZone, "impostorMethod", "inc")
end
-- declare all units as alive
theZone.allImpsDead = false
-- we end with group replaced by impostors
end
--
-- Spawning
--
-- REAL --> IMP
function impostors.turnGroupsIntoImpostors(theZone)
-- can be handed an array of strings or groups.
-- returns a dict of impostors, indexed by group names
local theGroupNames = theZone.groupNames
local myImpostors = {}
for idx, aGroupName in pairs(theGroupNames) do
local gName = aGroupName
if type(gName) == "table" then -- this is a group. get its name
trigger.action.outText("+++ipst: converting table gName to string in turnGroupsIntoImpostors",30)
gName = gName:getName()
end
if not gName then
trigger.action.outText("+++ipst: nil group name in turnGroupsIntoImpostors",30)
return nil
end
local aGroup = Group.getByName(gName)
if aGroup and gName then
-- record unit data to create impostors
local rawData, cat, ctry = impostors.getRawDataFromGroupNamed(gName)
-- if we are tracking the group, remove it from tracker
if theZone.trackWith and groupTracker.removeGroupNamedFromTrackerNamed then
groupTracker.removeGroupNamedFromTrackerNamed(gName, theZone.trackWith)
end
-- despawn the group. we'll spawn statics now
-- we may do some book-keeping first for the
-- names. we'll see later
Group.destroy(aGroup)
-- local rawData, cat, ctry = cfxMX.getGroupFromDCSbyName(gName)
-- local origID = rawData.groupId -- may be redundant
-- now spawn impostors based on the rawData,
-- and return impostorGroup
local impostorGroup = impostors.spawnImpostorsFromData(rawData, cat, ctry)
myImpostors[gName] = impostorGroup
end
end
return myImpostors
end
function impostors.turnZoneIntoImpostors(theZone)
if theZone.verbose then
trigger.action.outText("+++ipst: creating impostors for zone <" .. theZone.name ..">", 30)
end
theZone.myImpostors = impostors.turnGroupsIntoImpostors(theZone)
if theZone.myImpostors then
theZone.impostor = true
else
if theZone.verbose or impostors.verbose then
trigger.action.outText("+++ipst: groups to impostors failed for <" .. theZone.name .. ">",30)
end
end
end
-- IMP --> REAL
function impostors.relinkZonesForGroup(relinkZones, newGroup)
-- may be called sync and async!
local allUnits = newGroup:getUnits()
for idx, theUnit in pairs(allUnits) do
local unitName = theUnit:getName()
local linkedZones = relinkZones[unitName]
if linkedZones and #linkedZones > 0 then
for idy, theZone in pairs(linkedZones) do
theZone.linkedUnit = theUnit
if theZone.verbose then
trigger.action.outText("+++ipst: re-linked zone <" .. theZone.name .. "> to unit <" .. unitName .. ">", 30)
end
end
end
end
end
function impostors.spawnGroupsFromImpostor(theZone)
-- turn zone's impostors (static objects) into units
if theZone.verbose or impostors.verbose then
trigger.action.outText("+++ipst: spawning for impostor <" .. theZone.name .. ">", 30)
end
if not theZone.impostor then
if theZone.verbose or impostors.verbose then
trigger.action.outText("+++ipst: <> groups are not impostors.", 30)
end
return
end
local deadUnits = {} -- collect all dead units for immediate delete
-- after spawning
for idx, groupName in pairs(theZone.groupNames) do
-- get my group data from MX based on my name
-- we get from MX so we get all path and order info
local rawData, cat, ctry = cfxMX.getGroupFromDCSbyName(groupName)
local impostorGroup = theZone.myImpostors[groupName]
local relinkZones = {}
-- now iterate all units in that group, and remove their impostors
for idy, theUnit in pairs(rawData.units) do
local impName = impostorGroup[theUnit.name]
local impStat = StaticObject.getByName(impName)
if impStat and impStat:isExist() and impStat:getLife() > 1 then
-- still alive. read x, y and heading
local sp = impStat:getPoint()
theUnit.x = sp.x
theUnit.y = sp.z -- !!!
theUnit.heading = dcsCommon.getUnitHeading(impStat) -- should also work for statics
-- should automatically handle ["livery_id"]
relinkZones[theUnit.name] = cfxZones.zonesLinkedToUnit(impStat)
else
-- dead
table.insert(deadUnits, theUnit.name)
end
-- destroy imp
if impStat and impStat:isExist() then
impStat:destroy()
end
end
-- destroy impostor info
theZone.myImpostors[groupName] = nil
theZone.impostor = false
-- now create the group
if theZone.blinkTime <= 0 then
-- immediate spawn
local newGroup = coalition.addGroup(ctry, cfxMX.catText2ID(cat), rawData)
impostors.relinkZonesForGroup(relinkZones, newGroup)
if theZone.trackWith and groupTracker.addGroupToTrackerNamed then
-- add these groups to the group tracker
if theZone.verbose or impostors.verbose then
trigger.action.outText("+++ipst: attempting to add group <" .. newGroup:getName() .. "> to tracker <" .. theZone.trackWith .. ">", 30)
end
groupTracker.addGroupToTrackerNamed(newGroup, theZone.trackWith)
end
else
-- scheduled spawn
theZone.blinkCount = theZone.blinkCount + 1 -- so healthcheck avoids false positives
local args = {}
args.ctry = ctry
args.cat = cfxMX.catText2ID(cat)
args.rawData = rawData
args.theZone = theZone
args.relinkZones = relinkZones
timer.scheduleFunction(impostors.delayedSpawn, args, timer.getTime() + theZone.blinkTime)
end
end
-- now remove all dead units
if theZone.blinkTime <= 0 then
for idx, unitName in pairs(deadUnits) do
local theUnit = Unit.getByName(unitName)
if theUnit then
theUnit:destroy() -- BAD BAD BAD!!!! do some guarding, mon!
end
end
else
-- schedule removal of all dead units for later
timer.scheduleFunction(impostors.delayedCleanup, deadUnits, timer.getTime() + theZone.blinkTime + 0.1)
end
theZone.myImpostors = nil
end
function impostors.delayedSpawn(args)
local rawData = args.rawData
local cat = args.cat
local ctry = args.ctry
local theZone = args.theZone
local relinkZones = args.relinkZones
if theZone.verbose or impostors.verbose then
trigger.action.outText("+++ipst: delayed spawn for group <" .. rawData.name .. "> of zone <" .. theZone.name .. ">", 30)
end
local newGroup = coalition.addGroup(ctry, cat, rawData)
impostors.relinkZonesForGroup(relinkZones, newGroup)
if newGroup then
-- trigger.action.outText("+++ipst: SUCCESS!!! Spawned group <" .. newGroup:getName() .. "> for impostor <" .. rawData.name .. ">", 30)
else
trigger.action.outText("+++ipst: failed to spawn group for impostor <" .. rawData.name .. ">", 30)
end
if theZone.trackWith and groupTracker.addGroupToTrackerNamed then
-- add these groups to the group tracker
if theZone.verbose or impostors.verbose then
trigger.action.outText("+++ipst: attempting to add group <" .. newGroup:getName() .. "> to tracker <" .. theZone.trackWith .. ">", 30)
end
groupTracker.addGroupToTrackerNamed(newGroup, theZone.trackWith)
end
-- close TRX bracket for blinking, health check can proceed
theZone.blinkCount = theZone.blinkCount - 1
end
function impostors.delayedCleanup(deadUnits)
for idx, unitName in pairs(deadUnits) do
local theUnit = Unit.getByName(unitName)
if theUnit then
theUnit:destroy() -- BAD BAD BAD!!!! do some guarding, mon!
end
end
end
--
-- healthCheck
--
function impostors.healthCheck(theZone)
-- make sure there is at least one living unit left
-- if not, bang!
if theZone.allImpsDead then return end -- we are already dead
if theZone.impostor then
-- we have impostors. Check until you find the first live ones
for gName, impNames in pairs (theZone.myImpostors) do
-- impNames are the names of all the static objects
-- in this group
for idx, theImpName in pairs (impNames) do
local theImp = StaticObject.getByName(theImpName)
if theImp and theImp:isExist() then
local life = theImp:getLife()
if life > 1 then
-- all is well, at least one imp alive
return
end
end
end
end
-- when we get here, all imps are dead,
-- drop through
if theZone.verbose or impostors.verbose then
trigger.action.outText("+++ipst: Zone <" .. theZone.name .. "> - all impostors destroyed. Removing.", 30)
end
else
-- we have real groups. Let's iterate
if theZone.blinkCount > 0 then return end -- blinking, no healtch check
theZone.BlinkCount = 0 -- just in case it went negative
for idx, groupName in pairs(theZone.groupNames) do
local theGroup = Group.getByName(groupName)
if theGroup and theGroup:isExist() then
local allUnits = theGroup:getUnits()
for idy, aUnit in pairs (allUnits) do
if aUnit:isExist() then
local life = aUnit:getLife()
if life > 1 then
return -- all is well
end
end
end
end
end
-- if we get here, all units ded
if theZone.verbose or impostors.verbose then
trigger.action.outText("+++ipst: Zone <" .. theZone.name .. "> - all active units destroyed. Removing.", 30)
end
end
-- when we get here , all are ded
theZone.allImpsDead = true
if theZone.allDead then
cfxZones.pollFlag(theZone.allDead, theZone.impostorMethod, theZone)
end
end
--
-- Update
--
function impostors.update()
timer.scheduleFunction(impostors.update, {}, timer.getTime() + 1/impostors.ups)
for idx, aZone in pairs(impostors.impostorZones) do
-- first perform health check on all zones
impostors.healthCheck(aZone)
-- now see if we received signals
if not aZone.allImpsDead then
-- see if we got impostor? command
if cfxZones.testZoneFlag(aZone, aZone.impostorFlag, aZone.impostorTriggerMethod, "lastImpostorValue") then
if impostors.verbose or aZone.verbose then
trigger.action.outText("+++ipst: turn group to impostors triggered for <" .. aZone.name .. "> on <" .. aZone.impostorFlag .. ">", 30)
end
impostors.turnZoneIntoImpostors(aZone)
end
if aZone.reanimateFlag and cfxZones.testZoneFlag(aZone, aZone.reanimateFlag, aZone.impostorTriggerMethod, "lastReanimateValue") then
if impostors.verbose or aZone.verbose then
trigger.action.outText("+++ipst: impostor to live groups spawn triggered for <" .. aZone.name .. "> on <" .. aZone.impostorFlag .. ">", 30)
end
impostors.spawnGroupsFromImpostor(aZone)
end
else
-- nothing to do, all dead
end
end
end
--
-- start
--
function impostors.readConfigZone()
local theZone = cfxZones.getZoneByName("impostorsConfig")
if not theZone then
if impostors.verbose then
trigger.action.outText("+++ipst: NO config zone!", 30)
end
theZone = cfxZones.createSimpleZone("impostorsConfig")
end
impostors.ups = cfxZones.getNumberFromZoneProperty(theZone, "ups", 1)
impostors.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
if impostors.verbose then
trigger.action.outText("+++ipst: read config", 30)
end
end
function impostors.start()
-- lib check
if not dcsCommon.libCheck then
trigger.action.outText("cfx Impostors requires dcsCommon", 30)
return false
end
if not dcsCommon.libCheck("cfx Impostors",
impostors.requiredLibs) then
return false
end
-- read config
impostors.readConfigZone()
-- process cloner Zones
local attrZones = cfxZones.getZonesWithAttributeNamed("impostor?")
-- now create an rnd gen for each one and add them
-- to our watchlist
for k, aZone in pairs(attrZones) do
impostors.createImpostorWithZone(aZone) -- process attribute and add to zone
impostors.addImpostorZone(aZone) -- remember it so we can smoke it
end
-- start update
impostors.update()
trigger.action.outText("cfx Impostors v" .. impostors.version .. " started.", 30)
return true
end
-- let's go!
if not impostors.start() then
trigger.action.outText("cf/x Impostors aborted: missing libraries", 30)
impostors = nil
end
--[[--
To do
- reset? flag: will reset all to MX locationS
- add a zone's follow ability to impostors by allowing linkedUnit to work with impostors
--]]--