Version 2.0.2

New scribe, names, better csarManager
This commit is contained in:
Christian Franz 2024-02-08 13:04:35 +01:00
parent 38d6487de7
commit 61f33561fc
14 changed files with 2139 additions and 150 deletions

Binary file not shown.

Binary file not shown.

View File

@ -18,7 +18,8 @@ VERSION HISTORY
interpolate hits on dead when looking at kills and projectile does interpolate hits on dead when looking at kills and projectile does
not exist not exist
also sampling kill events also sampling kill events
1.1.1 - fixed reading smoke color for zone
minor clean-up
--]]-- --]]--
bombRange.bombs = {} -- live tracking bombRange.bombs = {} -- live tracking
bombRange.collector = {} -- post-impact collections for 0.5 secs bombRange.collector = {} -- post-impact collections for 0.5 secs
@ -63,6 +64,7 @@ function bombRange.createRange(theZone) -- has bombRange attribte to mark it
theZone.reportName = theZone:getBoolFromZoneProperty("reportName", false) theZone.reportName = theZone:getBoolFromZoneProperty("reportName", false)
theZone.smokeHits = theZone:getBoolFromZoneProperty("smokeHits", false) theZone.smokeHits = theZone:getBoolFromZoneProperty("smokeHits", false)
theZone.smokeColor = theZone:getSmokeColorStringFromZoneProperty("smokeColor", "blue") theZone.smokeColor = theZone:getSmokeColorStringFromZoneProperty("smokeColor", "blue")
theZone.smokeColor = dcsCommon.smokeColor2Num(theZone.smokeColor)
theZone.flagHits = theZone:getBoolFromZoneProperty("flagHits", false) theZone.flagHits = theZone:getBoolFromZoneProperty("flagHits", false)
theZone.flagType = theZone:getStringFromZoneProperty("flagType", "Red_Flag") theZone.flagType = theZone:getStringFromZoneProperty("flagType", "Red_Flag")
theZone.clipDist = theZone:getNumberFromZoneProperty("clipDist", 2000) -- when further way, the drop will be disregarded theZone.clipDist = theZone:getNumberFromZoneProperty("clipDist", 2000) -- when further way, the drop will be disregarded
@ -99,7 +101,6 @@ function bombRange.getPlayerData(name)
theData.totalHits = 0 theData.totalHits = 0
theData.totalPercentage = 0 -- sum, must be divided by drops theData.totalPercentage = 0 -- sum, must be divided by drops
bombRange.playerData[name] = theData bombRange.playerData[name] = theData
-- trigger.action.outText("created new player data for " .. name, 30)
end end
return theData return theData
end end
@ -174,12 +175,11 @@ function bombRange.showStatsForPlayer(pName, gID, unitName)
end end
trigger.action.outTextForGroup(gID, msg, 30) trigger.action.outTextForGroup(gID, msg, 30)
end end
-- --
-- unit UI -- unit UI
-- --
function bombRange.initCommsForUnit(theUnit) function bombRange.initCommsForUnit(theUnit)
local uName = theUnit:getName() local uName = theUnit:getName()
local pName = theUnit:getPlayerName() local pName = theUnit:getPlayerName()
@ -261,7 +261,7 @@ function bombRange.suspectedHit(weapon, target)
local theDesc = target:getDesc() local theDesc = target:getDesc()
local theType = theDesc.typeName -- getTypeName gets display name local theType = theDesc.typeName -- getTypeName gets display name
-- filter statics that we want to ignore -- filter statics that we want to ignore
for idx, aType in pairs(bombRange.filterTypes) do for idx, aType in pairs(bombRange.filterTypes) do
if theType == aType then if theType == aType then
return return
@ -285,7 +285,6 @@ function bombRange.suspectedHit(weapon, target)
bombRange.impacted(b, target) -- use this for impact bombRange.impacted(b, target) -- use this for impact
theID = b.ID theID = b.ID
hasfound = true hasfound = true
-- trigger.action.outText("susHit: filtering COLLECTED b <" .. b.name .. ">", 30)
end end
end end
if hasfound then if hasfound then
@ -305,8 +304,6 @@ function bombRange.suspectedHit(weapon, target)
b.pos = weapon:getPoint() b.pos = weapon:getPoint()
b.v = weapon:getVelocity() b.v = weapon:getVelocity()
bombRange.impacted(b, target) bombRange.impacted(b, target)
-- trigger.action.outText("susHit: filtering live b <" .. b.name .. ">", 30)
else else
table.insert(filtered, b) table.insert(filtered, b)
end end
@ -363,7 +360,6 @@ function bombRange.suspectedKill(target)
end end
bombRange.bombs = filtered bombRange.bombs = filtered
if hasfound then if hasfound then
-- trigger.action.outText("protocol: removed LIVING weapon from roster after impacted() invocation for non-nil target in suspectedKill", 30)
return return
end end
@ -383,7 +379,6 @@ function bombRange.suspectedKill(target)
end end
if hasfound then -- remove from collector, hit attributed if hasfound then -- remove from collector, hit attributed
bombRange.collector[theID] = nil -- remove from collector bombRange.collector[theID] = nil -- remove from collector
-- trigger.action.outText("protocol: removed COLL weapon from roster after impacted() invocation for non-nil target in suspectedKill", 30)
return return
end end
end end
@ -478,14 +473,8 @@ function bombRange.impacted(weapon, target, finalPass)
if not targetName then targetName = target:getTypeName() end if not targetName then targetName = target:getTypeName() end
end end
-- local s = "Entering impacted() with weapon = <" .. weapon.name .. ">"
-- if target then
-- s = s .. " AND target = <" .. targetName .. ">"
-- end
-- when we enter, weapon has ipacted target - if target is non-nil -- when we enter, weapon has ipacted target - if target is non-nil
-- what we need to determine is if that target is inside a zone -- what we need to determine is if that target is inside a zone
local ipos = weapon.pos -- default to weapon location local ipos = weapon.pos -- default to weapon location
if target then if target then
ipos = target:getPoint() -- we make the target loc the impact point ipos = target:getPoint() -- we make the target loc the impact point
@ -542,32 +531,7 @@ function bombRange.impacted(weapon, target, finalPass)
end end
local impactInside = theRange:pointInZone(ipos) local impactInside = theRange:pointInZone(ipos)
--[[--
if target and (not impactInside) then
trigger.action.outText("Hit on target <" .. targetName .. "> outside of zone <" .. theRange.name .. ">. should exit unless final impact", 30)
-- find closest range to object that was hit
local closest = nil
local shortest = math.huge
local tp = target:getPoint()
for idx, aRange in pairs(bombRange.ranges) do
local zp = aRange:getPoint()
local zDist = dcsCommon.distFlat(zp, tp)
if zDist < shortest then
shortest = zDist
closest = aRange
end
end
trigger.action.outText("re-check: closest range to target now is <" .. closest.name ..">", 30)
if closest:pointInZone(tp) then
trigger.action.outText("target <" .. targetName .. "> is INSIDE this range, d = <" .. math.floor(shortest) .. ">", 30)
else
trigger.action.outText("targed indeed outside, d = <" .. math.floor(shortest) .. ">", 30)
end
if finalPass then trigger.action.outText("IS final pass.", 30) end
end
--]]--
if theRange.reporter and theRange.details then if theRange.reporter and theRange.details then
local ipc = weapon.impacted local ipc = weapon.impacted
if not ipc then ipc = timer.getTime() end if not ipc then ipc = timer.getTime() end
@ -605,7 +569,6 @@ function bombRange.impacted(weapon, target, finalPass)
bombRange.addImpactForWeapon(weapon, true, percentage) bombRange.addImpactForWeapon(weapon, true, percentage)
else else
msg = "Outside target area" msg = "Outside target area"
-- if target then msg = msg .. " (EVEN THOUGH TGT = " .. target:getName() .. ")" end
if theRange.reportName then msg = msg .. " " .. theRange.name end if theRange.reportName then msg = msg .. " " .. theRange.name end
if theRange.details then msg = msg .. " (off-center by " .. math.floor(minDist *10)/10 .. " m)" end if theRange.details then msg = msg .. " (off-center by " .. math.floor(minDist *10)/10 .. " m)" end
msg = msg .. ", no hit." msg = msg .. ", no hit."
@ -623,7 +586,6 @@ function bombRange.uncollect(theID)
if b then if b then
bombRange.collector[theID] = nil bombRange.collector[theID] = nil
bombRange.impacted(b, nil, true) -- final pass bombRange.impacted(b, nil, true) -- final pass
-- trigger.action.outText("(final impact)", 30)
end end
end end
@ -640,7 +602,6 @@ function bombRange.updateBombs()
else else
-- put on collector to time out in 1 seconds to allow -- put on collector to time out in 1 seconds to allow
-- asynch hits to still register for this weapon in MP -- asynch hits to still register for this weapon in MP
-- bombRange.impacted(theWeapon)
theWeapon.impacted = timer.getTime() theWeapon.impacted = timer.getTime()
bombRange.collector[theWeapon.ID] = theWeapon -- bombRange.collector[theWeapon.ID] = theWeapon --
timer.scheduleFunction(bombRange.uncollect, theWeapon.ID, timer.getTime() + 1) timer.scheduleFunction(bombRange.uncollect, theWeapon.ID, timer.getTime() + 1)
@ -766,6 +727,6 @@ if not bombRange.start() then
bombRange = nil bombRange = nil
end end
-- -- To Do:
-- add persistence -- add persistence
-- --

View File

@ -1,5 +1,5 @@
cfxMX = {} cfxMX = {}
cfxMX.version = "2.0.0" cfxMX.version = "2.0.1"
cfxMX.verbose = false cfxMX.verbose = false
--[[-- --[[--
Mission data decoder. Access to ME-built mission structures Mission data decoder. Access to ME-built mission structures
@ -11,6 +11,8 @@ cfxMX.verbose = false
- train carve-outs for vehicles - train carve-outs for vehicles
2.0.0 - clean-up 2.0.0 - clean-up
- harmonized with cfxGroups - harmonized with cfxGroups
2.0.1 - groupHotByName
--]]-- --]]--
cfxMX.groupNamesByID = {} cfxMX.groupNamesByID = {}
@ -19,6 +21,7 @@ cfxMX.unitIDbyName = {}
cfxMX.groupDataByName = {} cfxMX.groupDataByName = {}
cfxMX.groupTypeByName = {} -- category of group: "helicopter", "plane", "ship"... cfxMX.groupTypeByName = {} -- category of group: "helicopter", "plane", "ship"...
cfxMX.groupCoalitionByName = {} cfxMX.groupCoalitionByName = {}
cfxMX.groupHotByName = {}
cfxMX.countryByName ={} -- county of group named cfxMX.countryByName ={} -- county of group named
cfxMX.linkByName = {} cfxMX.linkByName = {}
cfxMX.allFixedByName = {} cfxMX.allFixedByName = {}
@ -205,10 +208,17 @@ function cfxMX.createCrossReferences()
local aID = group_data.groupId local aID = group_data.groupId
-- get linkUnit info if it exists -- get linkUnit info if it exists
local linkUnit = nil local linkUnit = nil
local isHot = false
if group_data and group_data.route and group_data.route and group_data.route.points[1] then if group_data and group_data.route and group_data.route and group_data.route.points[1] then
linkUnit = group_data.route.points[1].linkUnit linkUnit = group_data.route.points[1].linkUnit
cfxMX.linkByName[aName] = linkUnit cfxMX.linkByName[aName] = linkUnit
local action = group_data.route.points[1].action
if action then
isHot = dcsCommon.stringEndsWith(action, "Hot")
end
end end
cfxMX.groupHotByName[aName] = isHot
if group_data.units[1] and group_data.units[1].type == "Train" then if group_data.units[1] and group_data.units[1].type == "Train" then
category = "train" category = "train"
obj_type_name = "train" obj_type_name = "train"

View File

@ -1,5 +1,5 @@
csarManager = {} csarManager = {}
csarManager.version = "3.0.0" csarManager.version = "3.1.0"
csarManager.ups = 1 csarManager.ups = 1
--[[-- VERSION HISTORY --[[-- VERSION HISTORY
@ -18,9 +18,19 @@ csarManager.ups = 1
- new event manager - new event manager
- no longer single-proccing pilots - no longer single-proccing pilots
- can also handle aircraft - isTroopCarrier - can also handle aircraft - isTroopCarrier
- 3.1.0 - integration with scribe
- expanded internal API: newMissionCB, invoked at addMission
- expanded internal API: removedMissionCB
- added *rnd as option for csarName (requires "names")
- missions are sorted by distance
- mission timeLimit range implemented
- update handles time limit (pickup only)
- inflight status reflects time limit but will not time out
- pickupSound option
- lostSound option
INTEGRATES AUTOMATICALLY WITH playerScore IF INSTALLED INTEGRATES AUTOMATICALLY WITH playerScore IF INSTALLED
INTEGRATES WITH LIMITE AIRFRAMES IF INSTALLED INTEGRATES WITH LIMITED AIRFRAMES IF INSTALLED
--]]-- --]]--
-- modules that need to be loaded BEFORE I run -- modules that need to be loaded BEFORE I run
@ -55,7 +65,9 @@ csarManager.vectoring = true -- provide bearing and range
-- callbacks -- callbacks
-- --
csarManager.csarCompleteCB = {} csarManager.csarCompleteCB = {}
csarManager.csarCreatedCB = {}
csarManager.csarRemoveCB = {}
csarManager.csarPickupCB = {}
-- --
-- CREATING A CSAR -- CREATING A CSAR
-- --
@ -84,7 +96,7 @@ function csarManager.createDownedPilot(theMission, existingUnit)
aLocation.z = newTargetZone.point.z aLocation.z = newTargetZone.point.z
aHeading = math.random(360)/360 * 2 * 3.1415 aHeading = math.random(360)/360 * 2 * 3.1415
end end
theMission.locations = {}
local theBoyGroup = dcsCommon.createSingleUnitGroup(theMission.name, local theBoyGroup = dcsCommon.createSingleUnitGroup(theMission.name,
"Soldier M4 GRG", -- "Soldier M4 GRG", "Soldier M4 GRG", -- "Soldier M4 GRG",
aLocation.x, aLocation.x,
@ -96,12 +108,17 @@ function csarManager.createDownedPilot(theMission, existingUnit)
Group.Category.GROUND, Group.Category.GROUND,
theBoyGroup) theBoyGroup)
if theBoyGroup then if theBoyGroup then
table.insert(theMission.locations, aLocation)
else else
trigger.action.outText("+++csar: FAILED to create csar!", 30) trigger.action.outText("+++csar: FAILED to create csar!", 30)
end end
else else
theMission.group = existingUnit:getGroup() theMission.group = existingUnit:getGroup()
local allUnits = theMission.group:getUnits()
for idx, aUnit in pairs(allUnits) do
local loc = aUnit:getPoint() -- warning: won't work if group newly allocated!
table.insert(theMission.locations, loc)
end
end end
-- we now use commands to send radio transmissions -- we now use commands to send radio transmissions
@ -117,10 +134,19 @@ end
function csarManager.createCSARMissionData(point, theSide, freq, name, numCrew, timeLimit, mapMarker, inRadius, parashootUnit) -- if parashootUnit is set, will not allocate new function csarManager.createCSARMissionData(point, theSide, freq, name, numCrew, timeLimit, mapMarker, inRadius, parashootUnit) -- if parashootUnit is set, will not allocate new
-- create a type -- create a type
if not timeLimit then timeLimit = -1 end -- if not timeLimit then timeLimit = -1 end
if not point then return nil end if not point then return nil end
local newMission = {} local newMission = {}
newMission.side = theSide newMission.side = theSide
-- if "names" module active, allow random names if name
-- equals "*rnd"
if names and names.uniqueFullName and name == "*rnd" then
name = names.uniqueFullName()
elseif name == "*rnd" then
trigger.action.outText("+++scar: '*rnd' name requires the 'name' module for randomization. Using 'John Smith'", 30)
name = "John Smith"
end
if dcsCommon.stringStartsWith(name, "downed ") then if dcsCommon.stringStartsWith(name, "downed ") then
-- remove "downed" - it will be added again later -- remove "downed" - it will be added again later
name = dcsCommon.removePrefix(name, "downed ") name = dcsCommon.removePrefix(name, "downed ")
@ -129,7 +155,7 @@ function csarManager.createCSARMissionData(point, theSide, freq, name, numCrew,
end end
end end
if not inRadius then inRadius = csarManager.rescueRadius end if not inRadius then inRadius = csarManager.rescueRadius end
newMission.name = name .. "-" .. csarManager.missionID -- make it uuid-capable newMission.name = name .. " (ID#" .. csarManager.missionID .. ")" -- make it uuid-capable
if csarManager.addPrefix then if csarManager.addPrefix then
newMission.name = "downed " .. newMission.name newMission.name = "downed " .. newMission.name
end end
@ -145,22 +171,40 @@ function csarManager.createCSARMissionData(point, theSide, freq, name, numCrew,
-- allocate units -- allocate units
csarManager.createDownedPilot(newMission, parashootUnit) csarManager.createDownedPilot(newMission, parashootUnit)
newMission.timeStamp = timer.getTime() -- now
-- set timeLimit if enabled
if timeLimit then
local theLimit = cfxZones.randomDelayFromPositiveRange(timeLimit[1], timeLimit[2]) * 60
-- trigger.action.outText("set time limit for mission to "..theLimit, 30)
newMission.expires = timer.getTime() + theLimit
end
-- update counter and return -- update counter and return
csarManager.missionID = csarManager.missionID + 1 csarManager.missionID = csarManager.missionID + 1
return newMission return newMission
end end
function csarManager.addMission(theMission) function csarManager.addMission(theMission)
-- trigger.action.outText("enter addMission", 30)
table.insert(csarManager.openMissions, theMission) table.insert(csarManager.openMissions, theMission)
csarManager.invokeNewMissionCallbacks(theMission)
end end
function csarManager.removeMission(theMission) function csarManager.removeMission(theMission, pickup)
if not pickup then pickup = false end
-- invoked when evacuee is PICKED UP!
if not theMission then return end if not theMission then return end
local newMissions = {} local newMissions = {}
for idx, aMission in pairs (csarManager.openMissions) do for idx, aMission in pairs (csarManager.openMissions) do
if aMission ~= theMission then if aMission ~= theMission then
table.insert(newMissions, aMission) table.insert(newMissions, aMission)
else else
-- csarManager.invokeRemovedMissionCallbacks(theMission)
if pickup then
csarManager.invokePickUpCallbacks(theMission)
end
end end
end end
csarManager.openMissions = newMissions -- this is the new batch csarManager.openMissions = newMissions -- this is the new batch
@ -291,15 +335,24 @@ function csarManager.successMission(who, where, theMission)
end end
end end
-- scribe.integration
if scribe then
local theUnit = Unit.getByName(who)
if theUnit and theUnit.getPlayerName then
local pName = theUnit:getPlayerName()
scribe.playerRescueComplete(pName)
end
end
trigger.action.outTextForCoalition(theMission.side, trigger.action.outTextForCoalition(theMission.side,
who .. " successfully evacuated " .. theMission.name .. " to " .. where .. "!", who .. " successfully evacuated " .. theMission.name .. " to " .. where .. "!",
30) 30)
-- now call callback for coalition side -- now call callback for coalition side
-- callback has format callback(coalition, success true/false, numberSaved, descriptionText) -- callback has format callback(coalition, success true/false, numberSaved, descriptionText, theMission)
csarManager.invokeCallbacks(theMission.side, true, 1, "success") csarManager.invokeCallbacks(theMission.side, true, 1, "success", theMission)
trigger.action.outSoundForCoalition(theMission.side, csarManager.actionSound) -- "Quest Snare 3.wav") trigger.action.outSoundForCoalition(theMission.side, csarManager.successSound)
if csarManager.csarRedDelivered and theMission.side == 1 then if csarManager.csarRedDelivered and theMission.side == 1 then
cfxZones.pollFlag(csarManager.csarRedDelivered, "inc", csarManager.configZone) cfxZones.pollFlag(csarManager.csarRedDelivered, "inc", csarManager.configZone)
@ -324,7 +377,6 @@ function csarManager.heloLanded(theUnit)
conf.unit = theUnit conf.unit = theUnit
local theGroup = theUnit:getGroup() local theGroup = theUnit:getGroup()
conf.id = theGroup:getID() conf.id = theGroup:getID()
--conf.id = theUnit:getID()
conf.currentState = 0 conf.currentState = 0
local thePoint = theUnit:getPoint() local thePoint = theUnit:getPoint()
local mySide = theUnit:getCoalition() local mySide = theUnit:getCoalition()
@ -435,7 +487,7 @@ function csarManager.heloLanded(theUnit)
args.unitName = myName args.unitName = myName
timer.scheduleFunction(csarManager.asynchSuccess, args, timer.getTime() + 3) timer.scheduleFunction(csarManager.asynchSuccess, args, timer.getTime() + 3)
csarManager.removeMission(theMission) csarManager.removeMission(theMission, true) -- picked up
table.insert(conf.troopsOnBoard, theMission) table.insert(conf.troopsOnBoard, theMission)
theMission.group:destroy() -- will shut up radio as well theMission.group:destroy() -- will shut up radio as well
theMission.group = nil theMission.group = nil
@ -466,7 +518,7 @@ function csarManager.asynchSuccess(args)
end end
function csarManager.asynchSound(args) function csarManager.asynchSound(args)
trigger.action.outSoundForCoalition(args.mySide, csarManager.actionSound) trigger.action.outSoundForCoalition(args.mySide, csarManager.pickupSound)
end end
-- --
-- --
@ -663,6 +715,7 @@ function csarManager.openMissionsForSide(theSide)
end end
function csarManager.doListCSARRequests(args) function csarManager.doListCSARRequests(args)
local now = timer.getTime()
local conf = args[1] local conf = args[1]
local param = args[2] local param = args[2]
local theUnit = conf.unit local theUnit = conf.unit
@ -675,14 +728,34 @@ function csarManager.doListCSARRequests(args)
if #openMissions < 1 then if #openMissions < 1 then
report = report .. "\nNo requests, all crew are safe." report = report .. "\nNo requests, all crew are safe."
else else
-- iterate through all troops onboard to get their status -- iterate through all missions and calc distance
for idx, mission in pairs(openMissions) do for idx, mission in pairs(openMissions) do
local d = dcsCommon.distFlat(point, mission.zone.point) * 0.000539957 local d = dcsCommon.distFlat(point, mission.zone.point) * 0.000539957
d = math.floor(d * 10) / 10 d = math.floor(d * 10) / 10
mission.dist = d
end
-- sort openMissions by dist
table.sort(openMissions,
function (e1, e2) return e1.dist < e2.dist end
)
-- we may want to limit to n nearest missions
local maxM = #openMissions
if maxM > csarManager.maxMissions then maxM = csarManager.maxMissions end
for i=1,maxM do --in pairs(openMissions) do
local mission = openMissions[i]
local b = dcsCommon.bearingInDegreesFromAtoB(point, mission.zone.point) local b = dcsCommon.bearingInDegreesFromAtoB(point, mission.zone.point)
local status = "alive" local status = "alive"
if mission.expires then
delta = math.floor ((mission.expires - now) / 60)
if delta < 10 then status = "+deteriorating+" end
if delta < 5 then status = "*critical*" end
if csarManager.verbose then
status = status .. "[" .. delta .. "]" -- remove me
end
end
if csarManager.vectoring then if csarManager.vectoring then
report = report .. "\n".. mission.name .. ", bearing " .. b .. ", " ..d .."nm, " .. " ADF " .. mission.freq * 10 .. " kHz - " .. status report = report .. "\n".. mission.name .. ", bearing " .. b .. ", " ..mission.dist .."nm, " .. " ADF " .. mission.freq * 10 .. " kHz - " .. status
else else
-- leave out vectoring -- leave out vectoring
report = report .. "\n".. mission.name .. " ADF " .. mission.freq * 10 .. " kHz - " .. status report = report .. "\n".. mission.name .. " ADF " .. mission.freq * 10 .. " kHz - " .. status
@ -707,6 +780,7 @@ function csarManager.doStatusCarrying(args)
local conf = args[1] local conf = args[1]
local param = args[2] local param = args[2]
local theUnit = conf.unit local theUnit = conf.unit
local now = timer.getTime()
-- build status report -- build status report
local report = "\nCrew Rescue Status:\n" local report = "\nCrew Rescue Status:\n"
@ -717,7 +791,18 @@ function csarManager.doStatusCarrying(args)
for i=1, #conf.troopsOnBoard do for i=1, #conf.troopsOnBoard do
local evacMission = conf.troopsOnBoard[i] local evacMission = conf.troopsOnBoard[i]
report = report .. "\n".. i .. ") " .. evacMission.name report = report .. "\n".. i .. ") " .. evacMission.name
report = report .. " is stable" -- or 'beat up, but will live' if evacMission.expires then
delta = math.floor ((mission.expires - now) / 60)
if delta > 20 then
report = report .. " is hurt but stable"
elseif delta > 10 then
report = report .. " is badly hurt"
else
report = report .. " is in critical condition" -- or 'beat up, but will live'
end
else
report = report .. " is stable" -- or 'beat up, but will live'
end
end end
report = report .. "\n\nTotal added weigth: " .. 10 + #conf.troopsOnBoard * csarManager.pilotWeight .. "kg" report = report .. "\n\nTotal added weigth: " .. 10 + #conf.troopsOnBoard * csarManager.pilotWeight .. "kg"
@ -886,16 +971,31 @@ end
-- --
function csarManager.updateCSARMissions() function csarManager.updateCSARMissions()
local newMissions = {} local newMissions = {}
local now = timer.getTime()
for idx, aMission in pairs (csarManager.openMissions) do for idx, aMission in pairs (csarManager.openMissions) do
-- see if mission timed out
local now = timer.getTime()
local stillRunning = true
if aMission.expires then stillRunning = aMission.expires > now end
local stillAlive = dcsCommon.isGroupAlive(aMission.group) local stillAlive = dcsCommon.isGroupAlive(aMission.group)
-- now check if a timer was running to rescue this group
-- if dead, set stillAlive to false -- if dead, set stillAlive to false
if stillAlive then if stillRunning and stillAlive then
table.insert(newMissions, aMission) table.insert(newMissions, aMission)
elseif stillAlive then
local msg = aMission.name .. " is no longer responding. Abort rescue."
trigger.action.outTextForCoalition(aMission.side, msg, 30)
trigger.action.outSoundForCoalition(aMission.side, csarManager.lostSound)
csarManager.invokeCallbacks(aMission.side, false, 1, "lost", aMission)
if aMission.group and Group.isExist(aMission.group) then
-- trigger.action.outText("removing group", 30)
Group.destroy(aMission.group)
end
else else
local msg = aMission.name .. " confirmed KIA, repeat KIA. Abort CSAR." local msg = aMission.name .. " confirmed KIA, repeat KIA. Abort CSAR."
trigger.action.outTextForCoalition(aMission.side, msg, 30) trigger.action.outTextForCoalition(aMission.side, msg, 30)
trigger.action.outSoundForCoalition(aMission.side, csarManager.actionSound) -- "Quest Snare 3.wav") trigger.action.outSoundForCoalition(aMission.side, csarManager.actionSound)
csarManager.invokeCallbacks(aMission.side, false, 1, "KIA", aMission)
end end
end end
csarManager.openMissions = newMissions -- this is the new batch csarManager.openMissions = newMissions -- this is the new batch
@ -926,12 +1026,11 @@ function csarManager.update() -- every second
-- now scan through all helo groups and see if they are close to a -- now scan through all helo groups and see if they are close to a
-- CSAR zone and initiate the help sequence -- CSAR zone and initiate the help sequence
-- local allPlayerGroups = cfxPlayerGroups -- cfxPlayerGroups is a global, don't fuck with it!
local allPlayerUnits = dcsCommon.getAllExistingPlayersAndUnits() -- indexed by player name local allPlayerUnits = dcsCommon.getAllExistingPlayersAndUnits() -- indexed by player name
--old -- contains per group a player record, use prime unit to access player's unit
for pname, aUnit in pairs(allPlayerUnits) do for pname, aUnit in pairs(allPlayerUnits) do
if --aUnit:isExist() and if aUnit:inAir() and
aUnit:inAir() and
dcsCommon.isTroopCarrier(aUnit, csarManager.troopCarriers) dcsCommon.isTroopCarrier(aUnit, csarManager.troopCarriers)
then -- troop carrier and is flying then -- troop carrier and is flying
local uPoint = aUnit:getPoint() local uPoint = aUnit:getPoint()
@ -1044,7 +1143,7 @@ function csarManager.update() -- every second
end end
--trigger.action.outTextForGroup(uID, hoverMsg, 30, true) --trigger.action.outTextForGroup(uID, hoverMsg, 30, true)
trigger.action.outSoundForGroup(uID, csarManager.actionSound) trigger.action.outSoundForGroup(uID, csarManager.pickupSound)
--return -- we only ever rescue one --return -- we only ever rescue one
end -- hovered long enough end -- hovered long enough
@ -1209,8 +1308,15 @@ function csarManager.readCSARZone(theZone)
if theZone.csarFreq < 0.01 then theZone.csarFreq = nil end if theZone.csarFreq < 0.01 then theZone.csarFreq = nil end
theZone.numCrew = 1 theZone.numCrew = 1
theZone.csarMapMarker = nil theZone.csarMapMarker = nil
theZone.timeLimit = theZone:getNumberFromZoneProperty("timeLimit", 0) if theZone:hasProperty("timeLimit") then
if theZone.timeLimit == 0 then theZone.timeLimit = nil else theZone.timeLimit = timeLimit * 60 end local tmin, tmax = theZone:getPositiveRangeFromZoneProperty("timeLimit", 1)
-- trigger.action.outText("Read time limit for <" .. theZone.name .. ">: <" .. tmin .. ">, <" .. tmax .. ">", 30)
theZone.timeLimit = {tmin, tmax}
else
theZone.timeLimit = nil
end
-- theZone.timeLimit = theZone:getNumberFromZoneProperty("timeLimit", 0)
-- if theZone.timeLimit == 0 then theZone.timeLimit = nil else theZone.timeLimit = timeLimit * 60 end
local deferred = theZone:getBoolFromZoneProperty("deferred", false) local deferred = theZone:getBoolFromZoneProperty("deferred", false)
@ -1255,7 +1361,7 @@ function csarManager.readCSARZone(theZone)
-- add to list of startable csar -- add to list of startable csar
if theZone.startCSAR then if theZone.startCSAR then
csarManager.addCSARZone(theZone) csarManager.addCSARZone(theZone)
trigger.action.outText("csar: added <".. theZone.name .."> to deferred csar missions", 30) -- trigger.action.outText("csar: added <".. theZone.name .."> to deferred csar missions", 30)
end end
if deferred and not theZone.startCSAR then if deferred and not theZone.startCSAR then
@ -1276,11 +1382,29 @@ end
-- Init & Start -- Init & Start
-- --
function csarManager.invokeCallbacks(theCoalition, success, numRescued, notes) -- mission complete cs(coalition, success, numberRescued, notes, data)
function csarManager.invokeCallbacks(theCoalition, success, numRescued, notes, theMission)
-- invoke anyone who wants to know that a group -- invoke anyone who wants to know that a group
-- of people was rescued. -- of people was rescued.
for idx, cb in pairs(csarManager.csarCompleteCB) do for idx, cb in pairs(csarManager.csarCompleteCB) do
cb(theCoalition, success, numRescued, notes) cb(theCoalition, success, numRescued, notes, theMission)
end
end
-- mission created cb(theMission)
function csarManager.invokeNewMissionCallbacks(theMission)
--trigger.action.outText("enter invoke new mission cb", 30)
-- invoke anyone who wants to know that a new mission was created
for idx, cb in pairs(csarManager.csarCreatedCB) do
cb(theMission)
end
end
-- mission: picking up the evacuee
function csarManager.invokePickUpCallbacks(theMission)
-- invoke anyone who wants to know that a new mission was created
for idx, cb in pairs(csarManager.csarPickupCB) do
cb(theMission)
end end
end end
@ -1288,6 +1412,14 @@ function csarManager.installCallback(theCB)
table.insert(csarManager.csarCompleteCB, theCB) table.insert(csarManager.csarCompleteCB, theCB)
end end
function csarManager.installNewMissionCallback(theCB)
table.insert(csarManager.csarCreatedCB, theCB)
end
function csarManager.installPickupCallback(theCB)
table.insert(csarManager.csarPickupCB, theCB)
end
function csarManager.readConfigZone() function csarManager.readConfigZone()
csarManager.name = "csarManagerConfig" -- compat with cfxZones csarManager.name = "csarManagerConfig" -- compat with cfxZones
local theZone = cfxZones.getZoneByName("csarManagerConfig") local theZone = cfxZones.getZoneByName("csarManagerConfig")
@ -1331,7 +1463,10 @@ function csarManager.readConfigZone()
csarManager.rescueScore = theZone:getNumberFromZoneProperty( "rescueScore", 100) csarManager.rescueScore = theZone:getNumberFromZoneProperty( "rescueScore", 100)
csarManager.actionSound = theZone:getStringFromZoneProperty( "actionSound", "Quest Snare 3.wav") csarManager.actionSound = theZone:getStringFromZoneProperty( "actionSound", "Quest Snare 3.wav")
csarManager.successSound = theZone:getStringFromZoneProperty("successSound", csarManager.actionSound)
csarManager.pickupSound = theZone:getStringFromZoneProperty("pickupSound", csarManager.actionSound)
csarManager.vectoring = theZone:getBoolFromZoneProperty("vectoring", true) csarManager.vectoring = theZone:getBoolFromZoneProperty("vectoring", true)
csarManager.lostSound = theZone:getStringFromZoneProperty("lostSound", csarManager.actionSound)
-- add own troop carriers -- add own troop carriers
if theZone:hasProperty("troopCarriers") then if theZone:hasProperty("troopCarriers") then
@ -1348,6 +1483,7 @@ function csarManager.readConfigZone()
csarManager.addPrefix = theZone:getBoolFromZoneProperty("addPrefix", true) csarManager.addPrefix = theZone:getBoolFromZoneProperty("addPrefix", true)
csarManager.maxMissions = theZone:getNumberFromZoneProperty("maxMissions", 15)
if csarManager.verbose then if csarManager.verbose then
trigger.action.outText("+++csar: read config", 30) trigger.action.outText("+++csar: read config", 30)
end end
@ -1415,4 +1551,6 @@ end
-- minFreq, maxFreq settings for config and mission-individual -- minFreq, maxFreq settings for config and mission-individual
-- may want to change if time limit was exceeded on return to tell
player that they did not survive the transport
--]]-- --]]--

View File

@ -1,5 +1,5 @@
dcsCommon = {} dcsCommon = {}
dcsCommon.version = "3.0.1" dcsCommon.version = "3.0.2"
--[[-- VERSION HISTORY --[[-- VERSION HISTORY
3.0.0 - removed bad bug in stringStartsWith, only relevant if caseSensitive is false 3.0.0 - removed bad bug in stringStartsWith, only relevant if caseSensitive is false
- point2text new intsOnly option - point2text new intsOnly option
@ -9,6 +9,8 @@ dcsCommon.version = "3.0.1"
- createGroundGroupWithUnits now supports liveries - createGroundGroupWithUnits now supports liveries
- new getAllExistingPlayersAndUnits() - new getAllExistingPlayersAndUnits()
3.0.1 - clone: better handling of string type 3.0.1 - clone: better handling of string type
3.0.2 - new getPlayerUnit()
3.0.3 - createStaticObjectForCoalitionInRandomRing() returns x and z
--]]-- --]]--
-- dcsCommon is a library of common lua functions -- dcsCommon is a library of common lua functions
@ -1934,7 +1936,7 @@ dcsCommon.version = "3.0.1"
function dcsCommon.createStaticObjectForCoalitionAtLocation(theCoalition, loc, name, objType, heading, dead) function dcsCommon.createStaticObjectForCoalitionAtLocation(theCoalition, loc, name, objType, heading, dead)
if not heading then heading = math.random(360) * 3.1415 / 180 end if not heading then heading = math.random(360) * 3.1415 / 180 end
local theData = dcsCommon.createStaticObjectDataAt(loc, name, objType, heading, dead) local theData = dcsCommon.createStaticObjectDataAt(loc, name, objType, heading, dead)
local theStatic = coalition.addStaticObject(theCoalition, theData) local theStatic = coalition.addStaticObject(theCoalition, theData) -- warning! coalition is not country!
return theStatic return theStatic
end end
@ -1947,8 +1949,8 @@ dcsCommon.version = "3.0.1"
theData.x = p.x theData.x = p.x
theData.y = p.z theData.y = p.z
local theStatic = coalition.addStaticObject(theCoalition, theData) local theStatic = coalition.addStaticObject(theCoalition, theData) -- warning! coalition is not country
return theStatic return theStatic, p.x, p.z
end end
@ -2703,6 +2705,19 @@ function dcsCommon.isTroopCarrier(theUnit, carriers)
return dcsCommon.isTroopCarrierType(uType, carriers) return dcsCommon.isTroopCarrierType(uType, carriers)
end end
function dcsCommon.getPlayerUnit(playerName)
if not playerName then return nil end
for idx, theSide in pairs(dcsCommon.coalitionSides) do
local thePlayers = coalition.getPlayers(theSide)
for idy, theUnit in pairs (thePlayers) do
if theUnit and theUnit:isExist() and theUnit.getPlayerName
and theUnit:getPlayerName() == playerName then
return theUnit
end
end
end
return nil
end
function dcsCommon.getAllExistingPlayerUnitsRaw() function dcsCommon.getAllExistingPlayerUnitsRaw()
local apu = {} local apu = {}

View File

@ -1,5 +1,5 @@
cfxHeloTroops = {} cfxHeloTroops = {}
cfxHeloTroops.version = "3.0.0" cfxHeloTroops.version = "3.0.2"
cfxHeloTroops.verbose = false cfxHeloTroops.verbose = false
cfxHeloTroops.autoDrop = true cfxHeloTroops.autoDrop = true
cfxHeloTroops.autoPickup = false cfxHeloTroops.autoPickup = false
@ -37,6 +37,8 @@ cfxHeloTroops.requestRange = 500 -- meters
- harmonized spawning invocations across cloners and spawners - harmonized spawning invocations across cloners and spawners
- dmlZones - dmlZones
- requestRange attribute - requestRange attribute
3.0.1 - fixed a bug with legalTroops attribute
3.0.2 - fixed a typo in in-air menu
--]]-- --]]--
-- --
@ -337,7 +339,7 @@ function cfxHeloTroops.addAirborneMenu(conf)
-- let's begin by assuming no troops aboard -- let's begin by assuming no troops aboard
local commandTxt = "(To load troops, land in proximity to them)" local commandTxt = "(To load troops, land in proximity to them)"
if conf.troopsOnBoardNum > 0 then if conf.troopsOnBoardNum > 0 then
commandTxt = "(You are carrying " .. conf.troopsOnBoardNum .. " Assault Troops. Land to deploy them" commandTxt = "(You are carrying " .. conf.troopsOnBoardNum .. " Assault Troops. Land to deploy them)"
end end
local theCommand = missionCommands.addCommandForGroup( local theCommand = missionCommands.addCommandForGroup(
conf.id, conf.id,
@ -887,7 +889,7 @@ function cfxHeloTroops.readConfigZone()
if theZone:hasProperty("legalTroops") then if theZone:hasProperty("legalTroops") then
local theTypesString = theZone:getStringFromZoneProperty("legalTroops", "") local theTypesString = theZone:getStringFromZoneProperty("legalTroops", "")
local unitTypes = dcsCommon.splitString(aSpawner.types, ",") local unitTypes = dcsCommon.splitString(theTypesString, ",")
if #unitTypes < 1 then 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 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 else

View File

@ -1,5 +1,5 @@
messenger = {} messenger = {}
messenger.version = "3.0.0" messenger.version = "3.1.0"
messenger.verbose = false messenger.verbose = false
messenger.requiredLibs = { messenger.requiredLibs = {
"dcsCommon", -- always "dcsCommon", -- always
@ -11,6 +11,8 @@ messenger.messengers = {}
2.3.0 - cfxZones OOP switch 2.3.0 - cfxZones OOP switch
2.3.1 - triggering message AFTER the on/off switches are tested 2.3.1 - triggering message AFTER the on/off switches are tested
3.0.0 - removed messenger, in?, f? attributes, harmonized on messenger? 3.0.0 - removed messenger, in?, f? attributes, harmonized on messenger?
3.1.0 - msgGroup supports multiple groups, separated by comma
- msgUnit supports multiple units, separated by comma
--]]-- --]]--
function messenger.addMessenger(theZone) function messenger.addMessenger(theZone)
@ -166,18 +168,12 @@ end
-- reat attributes -- reat attributes
-- --
function messenger.createMessengerWithZone(theZone) function messenger.createMessengerWithZone(theZone)
-- start val - a range
local aMessage = theZone:getStringFromZoneProperty("message", "") local aMessage = theZone:getStringFromZoneProperty("message", "")
theZone.message = aMessage -- refactoring: messenger.preProcMessage(aMessage, theZone) removed theZone.message = aMessage
theZone.spaceBefore = theZone:getBoolFromZoneProperty("spaceBefore", false) theZone.spaceBefore = theZone:getBoolFromZoneProperty("spaceBefore", false)
theZone.spaceAfter = theZone:getBoolFromZoneProperty("spaceAfter", false) theZone.spaceAfter = theZone:getBoolFromZoneProperty("spaceAfter", false)
theZone.soundFile = theZone:getStringFromZoneProperty("soundFile", "<none>") theZone.soundFile = theZone:getStringFromZoneProperty("soundFile", "<none>")
theZone.clearScreen = theZone:getBoolFromZoneProperty("clearScreen", false) theZone.clearScreen = theZone:getBoolFromZoneProperty("clearScreen", false)
theZone.duration = theZone:getNumberFromZoneProperty("duration", 30) theZone.duration = theZone:getNumberFromZoneProperty("duration", 30)
if theZone:hasProperty("messageDuration") then if theZone:hasProperty("messageDuration") then
theZone.duration = theZone:getNumberFromZoneProperty( "messageDuration", 30) theZone.duration = theZone:getNumberFromZoneProperty( "messageDuration", 30)
@ -188,28 +184,8 @@ function messenger.createMessengerWithZone(theZone)
if theZone:hasProperty("msgTriggerMethod") then if theZone:hasProperty("msgTriggerMethod") then
theZone.msgTriggerMethod = theZone:getStringFromZoneProperty("msgTriggerMethod", "change") theZone.msgTriggerMethod = theZone:getStringFromZoneProperty("msgTriggerMethod", "change")
end end
theZone.triggerMessagerFlag = theZone:getStringFromZoneProperty( "messenger?", "none")
if theZone:hasProperty("f?") then theZone.lastMessageTriggerValue = theZone:getFlagValue(theZone.triggerMessagerFlag) -- save last value
theZone.triggerMessagerFlag = theZone:getStringFromZoneProperty("f?", "none")
end
-- can also use in? for counting. we always use triggerMessagerFlag
if theZone:hasProperty("in?") then
theZone.triggerMessagerFlag = theZone:getStringFromZoneProperty("in?", "none")
end
--[[--
if theZone:hasProperty("messageOut?") then
theZone.triggerMessagerFlag = theZone:getStringFromZoneProperty("messageOut?", "none")
end
--]]--
-- try default only if no other is set
if not theZone.triggerMessagerFlag then
if not theZone:hasProperty("messenger?") then
trigger.action.outText("*** Note: messenger in <" .. theZone.name .. "> can't be triggered", 30)
end
theZone.triggerMessagerFlag = theZone:getStringFromZoneProperty( "messenger?", "none")
end
theZone.lastMessageTriggerValue = theZone:getFlagValue(theZone.triggerMessagerFlag)-- save last value
theZone.messageOff = theZone:getBoolFromZoneProperty("mute", false) --false theZone.messageOff = theZone:getBoolFromZoneProperty("mute", false) --false
if theZone:hasProperty("messageMute") then if theZone:hasProperty("messageMute") then
@ -235,15 +211,42 @@ function messenger.createMessengerWithZone(theZone)
end end
if theZone:hasProperty("group") then if theZone:hasProperty("group") then
theZone.msgGroup = theZone:getStringFromZoneProperty("group", "<none>") -- now supporting multiple groups
local theGroups = {}
local rawGroups = theZone:getStringFromZoneProperty("group", "<none>")
if dcsCommon.containsString(rawGroups, ",") then
theGroups = dcsCommon.splitString(rawGroups, ",")
theGroups = dcsCommon.trimArray(theGroups)
theZone.msgGroup = theGroups
else
theZone.msgGroup = {rawGroups}
end
elseif theZone:hasProperty("msgGroup") then elseif theZone:hasProperty("msgGroup") then
theZone.msgGroup = theZone:getStringFromZoneProperty("msgGroup", "<none>") local rawGroups = theZone:getStringFromZoneProperty("msgGroup", "<none>")
if dcsCommon.containsString(rawGroups, ",") then
theGroups = dcsCommon.splitString(rawGroups, ",")
theGroups = dcsCommon.trimArray(theGroups)
theZone.msgGroup = theGroups
else
theZone.msgGroup = {rawGroups,}
end
end end
if theZone:hasProperty("unit") then if theZone:hasProperty("unit") or theZone:hasProperty("msgUnit") then
theZone.msgUnit = theZone:getStringFromZoneProperty("unit", "<none>") local rawUnits = "err"
elseif theZone:hasProperty("msgUnit") then if theZone:hasProperty("unit") then
theZone.msgUnit = theZone:getStringFromZoneProperty("msgUnit", "<none>") rawUnits = theZone:getStringFromZoneProperty("unit", "<none>")
else
rawUnits = theZone:getStringFromZoneProperty("msgUnit", "<none>")
end
if dcsCommon.containsString(rawUnits, ",") then
local theUnits = dcsCommon.splitString(rawUnits, ",")
theUnits = dcsCommon.trimArray(theUnits)
theZone.msgUnit = theUnits
else
theZone.msgUnit = {rawUnits,}
end
end end
if (theZone.msgGroup and theZone.msgUnit) or if (theZone.msgGroup and theZone.msgUnit) or
@ -337,24 +340,28 @@ function messenger.isTriggered(theZone)
end end
trigger.action.outSoundForCoalition(theZone.msgCoalition, fileName) trigger.action.outSoundForCoalition(theZone.msgCoalition, fileName)
elseif theZone.msgGroup then elseif theZone.msgGroup then
local theGroup = Group.getByName(theZone.msgGroup) for idx, aGroupName in pairs(theZone.msgGroup) do
if theGroup and Group.isExist(theGroup) then local theGroup = Group.getByName(aGroupName)
local ID = theGroup:getID() if theGroup and Group.isExist(theGroup) then
msg = messenger.dynamicGroupProcessing(msg, theZone, theGroup) local ID = theGroup:getID()
if #msg > 0 or theZone.clearScreen then msg = messenger.dynamicGroupProcessing(msg, theZone, theGroup)
trigger.action.outTextForGroup(ID, msg, theZone.duration, theZone.clearScreen) if #msg > 0 or theZone.clearScreen then
trigger.action.outTextForGroup(ID, msg, theZone.duration, theZone.clearScreen)
end
trigger.action.outSoundForGroup(ID, fileName)
end end
trigger.action.outSoundForGroup(ID, fileName)
end end
elseif theZone.msgUnit then elseif theZone.msgUnit then
local theUnit = Unit.getByName(theZone.msgUnit) for idx, aUnitName in pairs (theZone.msgUnit) do
if theUnit and Unit.isExist(theUnit) then local theUnit = Unit.getByName(aUnitName)
local ID = theUnit:getID() if theUnit and Unit.isExist(theUnit) then
msg = messenger.dynamicUnitProcessing(msg, theZone, theUnit) local ID = theUnit:getID()
if #msg > 0 or theZone.clearScreen then msg = messenger.dynamicUnitProcessing(msg, theZone, theUnit)
trigger.action.outTextForUnit(ID, msg, theZone.duration, theZone.clearScreen) if #msg > 0 or theZone.clearScreen then
trigger.action.outTextForUnit(ID, msg, theZone.duration, theZone.clearScreen)
end
trigger.action.outSoundForUnit(ID, fileName)
end end
trigger.action.outSoundForUnit(ID, fileName)
end end
elseif theZone:getLinkedUnit() then elseif theZone:getLinkedUnit() then
-- this only works if the zone is linked to a unit -- this only works if the zone is linked to a unit

1234
modules/names.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@ -136,7 +136,7 @@ end
function ownAll.start() function ownAll.start()
-- lib check -- lib check
if not dcsCommon.libCheck then if not dcsCommon.libCheck then
trigger.action.outText("cfx ownAlll requires dcsCommon", 30) trigger.action.outText("cfx ownAll requires dcsCommon", 30)
return false return false
end end
if not dcsCommon.libCheck("cfx ownAll", ownAll.requiredLibs) then if not dcsCommon.libCheck("cfx ownAll", ownAll.requiredLibs) then

View File

@ -30,7 +30,6 @@ persistence.requiredLibs = {
persistence.flagsToSave = {} -- simple table persistence.flagsToSave = {} -- simple table
persistence.callbacks = {} -- cbblocks, dictionary by name persistence.callbacks = {} -- cbblocks, dictionary by name
-- --
-- modules register here -- modules register here
-- --
@ -76,7 +75,6 @@ function persistence.getSavedDataForModule(name)
return persistence.missionData[name] -- simply get the modules data block return persistence.missionData[name] -- simply get the modules data block
end end
-- --
-- Shared Data API -- Shared Data API
-- --
@ -113,7 +111,6 @@ function persistence.isDir(path) -- check if path is a directory
return success return success
end end
-- --
-- Main save meths -- Main save meths
-- --
@ -189,7 +186,6 @@ function persistence.saveTable(theTable, fileName, shared, append)
return true return true
end end
function persistence.loadText(fileName) -- load file as text function persistence.loadText(fileName) -- load file as text
if not persistence.active then return nil end if not persistence.active then return nil end
if not fileName then return nil end if not fileName then return nil end
@ -218,8 +214,6 @@ function persistence.loadTable(fileName) -- load file as table
return tab return tab
end end
-- --
-- Data Load on Start -- Data Load on Start
-- --
@ -401,7 +395,6 @@ end
-- --
-- config & start -- config & start
-- --
function persistence.collectFlagsFromZone(theZone) function persistence.collectFlagsFromZone(theZone)
local theFlags = theZone:getStringFromZoneProperty("saveFlags", "*dummy") local theFlags = theZone:getStringFromZoneProperty("saveFlags", "*dummy")
persistence.registerFlagsToSave(theFlags, theZone) persistence.registerFlagsToSave(theFlags, theZone)
@ -409,7 +402,7 @@ end
function persistence.readConfigZone() function persistence.readConfigZone()
if not _G["lfs"] then if not _G["lfs"] then
trigger.action.outText("+++persistence: DCS correctly not 'desanitized'. Persistence disabled", 30) trigger.action.outText("+++persistence: DCS currently not 'desanitized'. Persistence disabled", 30)
return return
end end

View File

@ -1,5 +1,5 @@
radioMenu = {} radioMenu = {}
radioMenu.version = "2.2.0" radioMenu.version = "2.2.1"
radioMenu.verbose = false radioMenu.verbose = false
radioMenu.ups = 1 radioMenu.ups = 1
radioMenu.requiredLibs = { radioMenu.requiredLibs = {
@ -18,6 +18,7 @@ radioMenu.menus = {}
full wildcard support for ack and cooldown full wildcard support for ack and cooldown
2.1.1 - outMessage now works correctly 2.1.1 - outMessage now works correctly
2.2.0 - clean-up 2.2.0 - clean-up
2.2.1 - corrected ackD
--]]-- --]]--
function radioMenu.addRadioMenu(theZone) function radioMenu.addRadioMenu(theZone)
@ -299,7 +300,7 @@ function radioMenu.createRadioMenuWithZone(theZone)
theZone.outValD = theZone:getStringFromZoneProperty("valD", 1) theZone.outValD = theZone:getStringFromZoneProperty("valD", 1)
end end
if theZone:hasProperty("ackD") then if theZone:hasProperty("ackD") then
theZone.ackC = theZone:getStringFromZoneProperty("ackD", "Acknowledged: D") theZone.ackD = theZone:getStringFromZoneProperty("ackD", "Acknowledged: D")
end end
if theZone:hasProperty("removeMenu?") then if theZone:hasProperty("removeMenu?") then

628
modules/scribe.lua Normal file
View File

@ -0,0 +1,628 @@
scribe = {}
scribe.version = "1.0.0"
scribe.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
"cfxMX",
}
--[[--
Player statistics package
VERSION HISTORY
1.0.0 Initial Version
--]]--
scribe.verbose = true
scribe.db = {} -- indexed by player name
scribe.playerUnits = {} -- indexed by unit name. for crash detection
--[[--
unitEntry:
ttime -- total time in seconds
airTime -- total air time
landings -- number of landings
lastLanding -- time of last landing OR DEPARTURE.
departures -- toital take-offs
crashes -- number of total crashes, deaths etc
--]]--
function scribe.createUnitEntry()
local theEntry = {}
theEntry.ttime = 0
theEntry.lastTime = 99999999 -- NOT math.huge because json
theEntry.landings = 0
theEntry.lastLanding = 99999999 -- not math.huge -- in the future
theEntry.departures = 0
theEntry.crashes = 0
theEntry.startUps = 0
theEntry.rescues = 0
return theEntry
end
--[[--
playerEntry:
units[] -- by type name indexed types the player has flown
-- each entry has
lastUnitName -- name of the unit player was last seen in
-- used to determine if player is still in-game
lastUnitType
isActive -- used to detect if player has left and close down record
--]]--
function scribe.createPlayerEntry(name)
local theEntry = {}
theEntry.playerName = name -- for easy access
local theUnit = dcsCommon.getPlayerUnit(name)
local theType = theUnit:getTypeName()
local unitName = theUnit:getName()
theEntry.units = {}
theEntry.lastUnitName = "cfxNone" -- don't ever use that name
theEntry.lastUnitType = "none"
theEntry.isActive = false -- is player in game?
return theEntry
end
function scribe.getPlayerNamed(name) -- lazy allocation
local theEntry = scribe.db[name]
if not theEntry then
theEntry = scribe.createPlayerEntry(name)
scribe.db[name] = theEntry
end
return theEntry
end
function scribe.sumPlayerEntry(theEntry, theField)
local sum = 0
for idx, aUnit in pairs(theEntry.units) do
if aUnit[theField] then sum = sum + aUnit[theField] end
end
return sum
end
function scribe.tickEntry(theEntry)
if not theEntry then return 0 end
local now = timer.getTime()
local uEntry = theEntry.units[theEntry.lastUnitType]
if not uEntry then return 0 end -- can happen on idling server that has reloaded. all last players have invalid last units
local delta = now - uEntry.lastTime
if delta < 0 then delta = 0 end
uEntry.lastTime = now
uEntry.ttime = uEntry.ttime + delta
return delta
end
function scribe.finalizeEntry(theEntry)
-- player no longer in game. finalize last entry
-- and make it inactive
theEntry.isActive = false
local delta = scribe.tickEntry(theEntry) -- on LAST flown aircraft
local uEntry = theEntry.units[theEntry.lastUnitType]
if uEntry then
uEntry.lastTime = 99999999 -- NOT math.huge
local deltaTime = dcsCommon.processHMS("<:h>:<:m>:<:s>", delta)
local fullTime = dcsCommon.processHMS("<:h>:<:m>:<:s>", uEntry.ttime)
if scribe.byePlayer then
trigger.action.outText("Player " .. theEntry.playerName .. " left " .. theEntry.lastUnitName .. " (a " .. theEntry.lastUnitType .. "), total time in aircraft " .. fullTime ..".", 30)
end
end
end
function scribe.entry2text(uEntry, totals)
-- validate uEntry, lazy init of missing fields
if not uEntry then uEntry = {} end
if not uEntry.ttime then uEntry.ttime = 0 end
if not uEntry.departures then uEntry.departures = 0 end
if not uEntry.landings then uEntry.landings = 0 end
if not uEntry.crashes then uEntry.crashes = 0 end
if not uEntry.startups then uEntry.startups = 0 end
if not uEntry.rescues then uEntry.rescues = 0 end
local t = ""
if not totals.ttime then totals.ttime = 0 end
t = t .. scribe.lTime .. " " .. dcsCommon.processHMS("<:h>:<:m>:<:s>", uEntry.ttime) .. " hrs"
totals.ttime = totals.ttime + uEntry.ttime
if scribe.departures then
t = t .. ", " .. scribe.lDeparture .. " " .. uEntry.departures
if not totals.departures then totals.departures = 0 end
totals.departures = totals.departures + uEntry.departures
end
if scribe.landings then
t = t .. ", " .. scribe.lLanding .. " " .. uEntry.landings
if not totals.landings then totals.landings = 0 end
totals.landings = totals.landings + uEntry.landings
end
if scribe.crashes then
t = t .. ", " .. scribe.lCrash .. " " .. uEntry.crashes
if not totals.crashes then totals.crashes = 0 end
totals.crashes = totals.crashes + uEntry.crashes
end
if scribe.startUps then
t = t .. ", " .. scribe.lStartUp .. " " .. uEntry.startUps
if not totals.startUps then totals.startUps = 0 end
totals.startUps = totals.startUps + uEntry.startUps
end
if scribe.rescues then
t = t .. ", " .. scribe.lRescue .. " " .. uEntry.rescues
if not totals.rescues then totals.rescues = 0 end
totals.rescues = totals.rescues + uEntry.rescues
end
return t
end
--
-- Event handling
--
function scribe.playerBirthedIn(playerName, theUnit)
-- access db
local theEntry = scribe.getPlayerNamed(playerName) -- can be new
local myType = theUnit:getTypeName()
local uName = theUnit:getName()
local theGroup = theUnit:getGroup()
local gID = theGroup:getID()
-- check if this player is still active
if theEntry.isActive then
-- do something to remedy this
scribe.finalizeEntry(theEntry)
end
-- check if player switched airframes
if theEntry.lastUnitName == uName and scribe.verbose then
trigger.action.outText("+++scb: player <" .. playerName .. "> reappeard in same unit <" .. uName .. ">", 30)
else
end
theEntry.lastUnitName = uName
theEntry.lastUnitType = myType
theEntry.isActive = true -- activate player
-- set us up to track this player in this unit
local myTypeEntry = theEntry.units[myType]
if not myTypeEntry then
myTypeEntry = scribe.createUnitEntry()
local uGroup = theUnit:getGroup()
local gName = uGroup:getName()
myTypeEntry.gName = gName
theEntry.units[myType] = myTypeEntry
end
myTypeEntry.lastTime = timer.getTime()
if scribe.verbose then
trigger.action.outText("+++scb: player <" .. playerName .. "> entered aircraft <" .. uName .. "> (a " .. myType .. ")", 30)
end
if scribe.greetPlayer then
local msg = "\nWelcome " .. theEntry.playerName .. " to your " .. myType .. ". Your stats currently are:\n\n"
msg = msg .. scribe.entry2data(theEntry) .. "\n"
trigger.action.outTextForGroup(gID, msg, 30)
end
end
function scribe.playerCrashed(playerName)
if scribe.verbose then
trigger.action.outText("+++scb: enter crash for <" .. playerName .. ">", 30)
end
local theEntry = scribe.getPlayerNamed(playerName)
if not theEntry.isActive then
if scribe.verbose then
trigger.action.outText("+++scb: player <" .. playerName .. "> CRASH event ignored: player not active", 30)
end
return
end
local uEntry = theEntry.units[theEntry.lastUnitType]
uEntry.crashes = uEntry.crashes + 1
scribe.finalizeEntry(theEntry)
end
function scribe.playerEjected(playerName)
if scribe.verbose then
trigger.action.outText("+++scb: enter eject for <" .. playerName .. ">, handing off to crash", 30)
end
-- counts as a crash
local theEntry = scribe.getPlayerNamed(playerName)
if not theEntry.isActive then
if scribe.verbose then
trigger.action.outText("+++scb: player <" .. playerName .. "> EJECT event ignored: player not active", 30)
end
return
end
scribe.playerCrashed(playerName)
end
function scribe.playerDied(playerName)
-- trigger.action.outText("+++scb: player <" .. playerName .. "> DEAD event, handing off to crashS", 30)
-- counts as a crash
local theEntry = scribe.getPlayerNamed(playerName)
if not theEntry.isActive then
if scribe.verbose then
trigger.action.outText("+++scb: player <" .. playerName .. "> DEAD event ignored: player not active", 30)
end
return
end
scribe.playerCrashed(playerName)
end
function scribe.engineStarted(playerName)
local theEntry = scribe.getPlayerNamed(playerName)
if not theEntry.isActive then
if scribe.verbose then
trigger.action.outText("+++scb: player <" .. playerName .. "> STARTUP event ignored: player not active", 30)
end
return
end
local uEntry = theEntry.units[theEntry.lastUnitType]
uEntry.startUps = uEntry.startUps + 1
if scribe.verbose then
trigger.action.outText("+++scb: startup registered for <" .. playerName .. ">.", 30)
end
end
function scribe.playerLanded(playerName)
local theEntry = scribe.getPlayerNamed(playerName)
if not theEntry.isActive then
if scribe.verbose then
trigger.action.outText("+++scb: player <" .. playerName .. "> landing event ignored: player not active", 30)
end
return
end
local uEntry = theEntry.units[theEntry.lastUnitType]
-- see if last landing is at least xx seconds old
local now = timer.getTime()
delta = now - uEntry.lastLanding
if delta > scribe.landingCD or delta < 0 then
uEntry.landings = uEntry.landings + 1
else
if scribe.verbose then
trigger.action.outText("+++scb: landing ignored: cooldown active", 30)
end
end
uEntry.lastLanding = now
end
function scribe.playerDeparted(playerName)
local theEntry = scribe.getPlayerNamed(playerName)
if not theEntry.isActive then
if scribe.verbose then
trigger.action.outText("+++scb: player <" .. playerName .. "> take-off event ignored: player not active", 30)
end
return
end
local uEntry = theEntry.units[theEntry.lastUnitType]
-- see if last landing is at least xx seconds old
local now = timer.getTime()
delta = now - uEntry.lastLanding -- we use laastLanding for BOTH!
if delta > scribe.landingCD or delta < 0 then
uEntry.departures = uEntry.departures + 1
else
if scribe.verbose then
trigger.action.outText("+++scb: departure ignored: cooldown active", 30)
end
end
uEntry.lastLanding = now -- also for Departures!
end
--
-- API
--
-- invoked from other modules
function scribe.playerRescueComplete(playerName)
local theEntry = scribe.getPlayerNamed(playerName)
if not theEntry.isActive then
if scribe.verbose then
trigger.action.outText("+++scb: player <" .. playerName .. "> rescue complete event ignored: player not active", 30)
end
return
end
local uEntry = theEntry.units[theEntry.lastUnitType]
if not uEntry then
-- this should not happen
trigger.action.outText("+scb: unknown unit for player <" .. playerName .. "> in recue complete. Ignored", 30)
return
end
if not uEntry.rescues then uEntry.rescues = 0 end
uEntry.rescues = uEntry.rescues + 1
end
function scribe:onEvent(theEvent)
if not theEvent.initiator then return end
local theUnit = theEvent.initiator
if not theUnit then return end
local uName = theUnit:getName()
if scribe.playerUnits[uName] and scribe.verbose then
trigger.action.outText("+++scb: event <" .. theEvent.id .. " = " .. dcsCommon.event2text(theEvent.id) .. ">, concerns player unit named <" .. uName .. ">.", 30)
end
if not theUnit.getPlayerName then
if scribe.playerUnits[uName] and scribe.verbose then
trigger.action.outText("+++scb no more a player unit (case A: getPlanerName not implemented), event = <" .. theEvent.id .. ">, unit named <" .. uName .. ">", 30)
end
return
end
local playerName = theUnit:getPlayerName()
if not playerName then
if scribe.playerUnits[uName] and scribe.verbose then
trigger.action.outText("+++scb no more a player unit (case B: nilplayer name), event = <" .. theEvent.id .. ">, unit named <" .. uName .. ">", 30)
end
return
end
-- when we get here we have a player event
-- players can only ever activate by birth event
if theEvent.id == 15 then -- birth
scribe.playerBirthedIn(playerName, theUnit)
scribe.playerUnits[uName] = playerName -- for crash helo detection
end
if theEvent.id == 8 or theEvent.id == 9 then -- dead, pilot_dead
scribe.playerDied(playerName)
end
if theEvent.id == 6 then -- ejected
scribe.playerEjected(playerName)
end
if theEvent.id == 5 then -- crash
scribe.playerCrashed(playerName)
end
if theEvent.id == 4 then -- landed
scribe.playerLanded(playerName)
end
if theEvent.id == 3 then -- take-off
scribe.playerDeparted(playerName)
-- trigger.action.outText("departure detected", 30)
end
if theEvent.id == 18 then -- engine start
-- make sure group isn't on hotstart
local theGroup = theUnit:getGroup()
local gName = theGroup:getName()
if cfxMX.groupHotByName[gName] then
if scribe.verbose then
trigger.action.outText("scb: ignored engine start: hot start for <" .. playerName .. ">", 30)
end
else
if scribe.verbose then
trigger.action.outText("scb: engine start for <" .. playerName .. ">", 30)
end
scribe.engineStarted(playerName)
end
end
end
--
-- GUI
--
function scribe.redirectCheckData(args)
timer.scheduleFunction(scribe.doCheckData, args, timer.getTime() + 0.1)
end
function scribe.doCheckData(unitInfo)
local unitName = unitInfo.uName
-- we now try and match player to the unit by rummaning through db
local thePlayerEntry = nil
for pName, theEntry in pairs(scribe.db) do
if unitName == theEntry.lastUnitName and theEntry.isActive then
thePlayerEntry = theEntry
end
end
if (not thePlayerEntry) then
if scribe.verbose then
trigger.action.outText("+++scb: cannot retrieve player for unit <" .. unitName .. ">", 30)
end
return
end
-- tick over so we have updated time
scribe.tickEntry(thePlayerEntry)
local msg = "Player " .. thePlayerEntry.playerName .. ":\n"
msg = msg .. scribe.entry2data(thePlayerEntry)
trigger.action.outTextForGroup(unitInfo.gID, msg, 30)
end
function scribe.entry2data(thePlayerEntry)
local msg = ""
local totals = {}
for aType, uEntry in pairs (thePlayerEntry.units) do
msg = msg .. aType .. " -- " .. scribe.entry2text(uEntry, totals) .. "\n"
end
if dcsCommon.getSizeOfTable(thePlayerEntry.units) > 1 then
local dummy = {}
msg = msg .. "\nTotals -- " .. scribe.entry2text(totals, dummy) .. "\n"
end
return msg
end
--
-- GC -- detect player leaving
--
function scribe.GC()
timer.scheduleFunction(scribe.GC, {}, timer.getTime() + 1)
-- iterate through all players in DB and see if they
-- are still on-line.
for pName, theEntry in pairs(scribe.db) do
if theEntry.isActive then
-- this player is on the books as in the game
local theUnit = Unit.getByName(theEntry.lastUnitName)
if theUnit and Unit.isExist(theUnit) and theUnit:getLife() >= 1 then
-- all is fine, go on
else
-- this unit no longer exists and we finalize player
if scribe.verbose then
trigger.action.outText("+++scb: player <" .. pName .. "> left <" .. theEntry.lastUnitName .. "> unit, finalizing", 30)
end
scribe.finalizeEntry(theEntry)
end
end
end
end
--
-- start
--
function scribe.startPlayerGUI()
-- scan all mx players
-- note: currently assumes single-player groups
-- in preparation of single-player 'commandForUnit'
-- ASSUMES SINGLE-UNIT PLAYER GROUPS!
for uName, uData in pairs(cfxMX.playerUnitByName) do
local unitInfo = {}
-- try and access each unit even if we know that the
-- unit does not exist in-game right now
local gData = cfxMX.playerUnit2Group[uName]
local gName = gData.name
local coa = cfxMX.groupCoalitionByName[gName]
local theType = uData.type
if scribe.verbose then
trigger.action.outText("unit <" .. uName .. ">: type <" .. theType .. "> coa <" .. coa .. ">, group <" .. gName .. ">", 30)
end
unitInfo.uName = uName -- needed for reverse-lookup
unitInfo.gName = gName -- also needed for reverse lookup
unitInfo.coa = coa
unitInfo.gID = gData.groupId
unitInfo.uID = uData.unitId
unitInfo.theType = theType
unitInfo.cat = cfxMX.groupTypeByName[gName]
unitInfo.root = missionCommands.addSubMenuForGroup(unitInfo.gID, scribe.uiMenu)
unitInfo.checkData = missionCommands.addCommandForGroup(unitInfo.gID, "Get Pilot's Statistics", unitInfo.root, scribe.redirectCheckData, unitInfo)
end
end
--
-- Config
--
function scribe.readConfigZone()
local theZone = cfxZones.getZoneByName("scribeConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("scribeConfig")
end
scribe.verbose = theZone.verbose
scribe.hasGUI = theZone:getBoolFromZoneProperty("hasGUI", true)
scribe.uiMenu = theZone:getStringFromZoneProperty("uiMenu", "Mission Logbook")
scribe.greetPlayer = theZone:getBoolFromZoneProperty("greetPlayer", true)
scribe.byePlayer = theZone:getBoolFromZoneProperty("byebyePlayer", true)
scribe.landings = theZone:getBoolFromZoneProperty("landings", true)
scribe.lLanding = theZone:getStringFromZoneProperty("lLandings", "landings:")
scribe.departures = theZone:getBoolFromZoneProperty("departures", true)
scribe.lDeparture = theZone:getStringFromZoneProperty("lDepartures", "take-offs:")
scribe.startUps = theZone:getBoolFromZoneProperty("startups", true)
scribe.lStartUp = theZone:getStringFromZoneProperty("lStartups", "starts:")
scribe.crashes = theZone:getBoolFromZoneProperty("crashes", true)
scribe.lCrash = theZone:getStringFromZoneProperty("lCrashes", "crashes:")
scribe.rescues = theZone:getBoolFromZoneProperty("rescues", false)
scribe.lRescue = theZone:getStringFromZoneProperty("lRescues", "rescues:")
scribe.lTime = theZone:getStringFromZoneProperty("lTime", "time:")
scribe.landingCD = theZone:getNumberFromZoneProperty("landingCD", 60) -- seconds between stake-off, landings, or either
end
--
-- load / save (game data)
--
function scribe.saveData()
local theData = {}
-- tick over all player entry recors so we can save
-- most recent data
for planerName, thePlayerEntry in pairs(scribe.db) do
if thePlayerEntry then scribe.tickEntry(thePlayerEntry) end
end
-- save current log. simple clone
local theLog = dcsCommon.clone(scribe.db)
theData.theLog = theLog
return theData
end
function scribe.loadData()
if not persistence then return end
local theData = persistence.getSavedDataForModule("scribe")
if not theData then
if scribe.verbose then
trigger.action.outText("+++scb: no save date received, skipping.", 30)
end
return
end
local theLog = theData.theLog
scribe.db = theLog
-- post-proc: set all to inactive, no player can be in game at start
for pName, theEntry in pairs(scribe.db) do
if theEntry.isActive then
theEntry.isActive = false
theEntry.lastUnitName = "cfxNone"
theEntry.lastUnitType = "xxx"
for uName, uEntry in pairs (theEntry.units) do
uEntry.lastTime = 99999999 -- NOT math.huge
uEntry.lastLanding = 99999999 -- NOT math.huge!
end
end
end
end
--
-- start
--
function scribe.start()
-- lib check
if not dcsCommon.libCheck then
trigger.action.outText("cfx scribe requires dcsCommon", 30)
return false
end
if not dcsCommon.libCheck("cfx scribe", scribe.requiredLibs) then
return false
end
-- install event handler
world.addEventHandler(scribe)
-- get config
scribe.readConfigZone()
-- install menus to all player units
if scribe.hasGUI then
scribe.startPlayerGUI()
end
-- now load all save data and populate map with troops that
-- we deployed last save.
if persistence then
-- sign up for persistence
callbacks = {}
callbacks.persistData = scribe.saveData
persistence.registerModule("scribe", callbacks)
-- now load my data
scribe.loadData()
end
-- start GC
timer.scheduleFunction(scribe.GC, {}, timer.getTime() + 1)
-- say hi!
trigger.action.outText("cfx scribe v" .. scribe.version .. " started.", 30)
return true
end
-- let's go
if not scribe.start() then
trigger.action.outText("cfx scribe module failed to launch.", 30)
end

Binary file not shown.