diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index aa18be0..f102daa 100644 Binary files a/Doc/DML Documentation.pdf and b/Doc/DML Documentation.pdf differ diff --git a/Doc/DML Quick Reference.pdf b/Doc/DML Quick Reference.pdf index 5e061d2..295811e 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/bombRange.lua b/modules/bombRange.lua index 5cea9b3..504a5ac 100644 --- a/modules/bombRange.lua +++ b/modules/bombRange.lua @@ -18,7 +18,8 @@ VERSION HISTORY interpolate hits on dead when looking at kills and projectile does not exist also sampling kill events - +1.1.1 - fixed reading smoke color for zone + minor clean-up --]]-- bombRange.bombs = {} -- live tracking 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.smokeHits = theZone:getBoolFromZoneProperty("smokeHits", false) theZone.smokeColor = theZone:getSmokeColorStringFromZoneProperty("smokeColor", "blue") + theZone.smokeColor = dcsCommon.smokeColor2Num(theZone.smokeColor) theZone.flagHits = theZone:getBoolFromZoneProperty("flagHits", false) theZone.flagType = theZone:getStringFromZoneProperty("flagType", "Red_Flag") 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.totalPercentage = 0 -- sum, must be divided by drops bombRange.playerData[name] = theData --- trigger.action.outText("created new player data for " .. name, 30) end return theData end @@ -173,13 +174,12 @@ function bombRange.showStatsForPlayer(pName, gID, unitName) end end trigger.action.outTextForGroup(gID, msg, 30) - - + end + -- -- unit UI -- - function bombRange.initCommsForUnit(theUnit) local uName = theUnit:getName() local pName = theUnit:getPlayerName() @@ -261,7 +261,7 @@ function bombRange.suspectedHit(weapon, target) local theDesc = target:getDesc() 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 if theType == aType then return @@ -285,7 +285,6 @@ function bombRange.suspectedHit(weapon, target) bombRange.impacted(b, target) -- use this for impact theID = b.ID hasfound = true --- trigger.action.outText("susHit: filtering COLLECTED b <" .. b.name .. ">", 30) end end if hasfound then @@ -305,8 +304,6 @@ function bombRange.suspectedHit(weapon, target) b.pos = weapon:getPoint() b.v = weapon:getVelocity() bombRange.impacted(b, target) - --- trigger.action.outText("susHit: filtering live b <" .. b.name .. ">", 30) else table.insert(filtered, b) end @@ -363,7 +360,6 @@ function bombRange.suspectedKill(target) end bombRange.bombs = filtered if hasfound then --- trigger.action.outText("protocol: removed LIVING weapon from roster after impacted() invocation for non-nil target in suspectedKill", 30) return end @@ -383,7 +379,6 @@ function bombRange.suspectedKill(target) end if hasfound then -- remove from collector, hit attributed 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 end end @@ -478,14 +473,8 @@ function bombRange.impacted(weapon, target, finalPass) if not targetName then targetName = target:getTypeName() 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 --- 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 if target then ipos = target:getPoint() -- we make the target loc the impact point @@ -542,32 +531,7 @@ function bombRange.impacted(weapon, target, finalPass) end 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 local ipc = weapon.impacted if not ipc then ipc = timer.getTime() end @@ -604,8 +568,7 @@ function bombRange.impacted(weapon, target, finalPass) bombRange.addImpactForWeapon(weapon, true, percentage) else - msg = "Outside target area" --- if target then msg = msg .. " (EVEN THOUGH TGT = " .. target:getName() .. ")" end + msg = "Outside target area" 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 msg = msg .. ", no hit." @@ -623,7 +586,6 @@ function bombRange.uncollect(theID) if b then bombRange.collector[theID] = nil bombRange.impacted(b, nil, true) -- final pass --- trigger.action.outText("(final impact)", 30) end end @@ -640,7 +602,6 @@ function bombRange.updateBombs() else -- put on collector to time out in 1 seconds to allow -- asynch hits to still register for this weapon in MP --- bombRange.impacted(theWeapon) theWeapon.impacted = timer.getTime() bombRange.collector[theWeapon.ID] = theWeapon -- timer.scheduleFunction(bombRange.uncollect, theWeapon.ID, timer.getTime() + 1) @@ -766,6 +727,6 @@ if not bombRange.start() then bombRange = nil end --- +-- To Do: -- add persistence -- \ No newline at end of file diff --git a/modules/cfxMX.lua b/modules/cfxMX.lua index c8269fa..bab4291 100644 --- a/modules/cfxMX.lua +++ b/modules/cfxMX.lua @@ -1,5 +1,5 @@ cfxMX = {} -cfxMX.version = "2.0.0" +cfxMX.version = "2.0.1" cfxMX.verbose = false --[[-- Mission data decoder. Access to ME-built mission structures @@ -11,6 +11,8 @@ cfxMX.verbose = false - train carve-outs for vehicles 2.0.0 - clean-up - harmonized with cfxGroups + 2.0.1 - groupHotByName + --]]-- cfxMX.groupNamesByID = {} @@ -19,6 +21,7 @@ cfxMX.unitIDbyName = {} cfxMX.groupDataByName = {} cfxMX.groupTypeByName = {} -- category of group: "helicopter", "plane", "ship"... cfxMX.groupCoalitionByName = {} +cfxMX.groupHotByName = {} cfxMX.countryByName ={} -- county of group named cfxMX.linkByName = {} cfxMX.allFixedByName = {} @@ -205,10 +208,17 @@ function cfxMX.createCrossReferences() local aID = group_data.groupId -- get linkUnit info if it exists local linkUnit = nil + local isHot = false 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 cfxMX.linkByName[aName] = linkUnit + local action = group_data.route.points[1].action + if action then + isHot = dcsCommon.stringEndsWith(action, "Hot") + end end + + cfxMX.groupHotByName[aName] = isHot if group_data.units[1] and group_data.units[1].type == "Train" then category = "train" obj_type_name = "train" diff --git a/modules/csarManager2.lua b/modules/csarManager2.lua index f65baa7..1fa631a 100644 --- a/modules/csarManager2.lua +++ b/modules/csarManager2.lua @@ -1,5 +1,5 @@ csarManager = {} -csarManager.version = "3.0.0" +csarManager.version = "3.1.0" csarManager.ups = 1 --[[-- VERSION HISTORY @@ -18,9 +18,19 @@ csarManager.ups = 1 - new event manager - no longer single-proccing pilots - 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 WITH LIMITE AIRFRAMES IF INSTALLED + INTEGRATES WITH LIMITED AIRFRAMES IF INSTALLED --]]-- -- modules that need to be loaded BEFORE I run @@ -55,7 +65,9 @@ csarManager.vectoring = true -- provide bearing and range -- callbacks -- csarManager.csarCompleteCB = {} - +csarManager.csarCreatedCB = {} +csarManager.csarRemoveCB = {} +csarManager.csarPickupCB = {} -- -- CREATING A CSAR -- @@ -84,7 +96,7 @@ function csarManager.createDownedPilot(theMission, existingUnit) aLocation.z = newTargetZone.point.z aHeading = math.random(360)/360 * 2 * 3.1415 end - + theMission.locations = {} local theBoyGroup = dcsCommon.createSingleUnitGroup(theMission.name, "Soldier M4 GRG", -- "Soldier M4 GRG", aLocation.x, @@ -96,12 +108,17 @@ function csarManager.createDownedPilot(theMission, existingUnit) Group.Category.GROUND, theBoyGroup) if theBoyGroup then - + table.insert(theMission.locations, aLocation) else trigger.action.outText("+++csar: FAILED to create csar!", 30) end else 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 -- 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 -- create a type - if not timeLimit then timeLimit = -1 end +-- if not timeLimit then timeLimit = -1 end if not point then return nil end local newMission = {} 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 -- remove "downed" - it will be added again later name = dcsCommon.removePrefix(name, "downed ") @@ -129,7 +155,7 @@ function csarManager.createCSARMissionData(point, theSide, freq, name, numCrew, end 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 newMission.name = "downed " .. newMission.name end @@ -145,22 +171,40 @@ function csarManager.createCSARMissionData(point, theSide, freq, name, numCrew, -- allocate units 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 csarManager.missionID = csarManager.missionID + 1 + return newMission end function csarManager.addMission(theMission) +-- trigger.action.outText("enter addMission", 30) table.insert(csarManager.openMissions, theMission) + csarManager.invokeNewMissionCallbacks(theMission) 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 local newMissions = {} for idx, aMission in pairs (csarManager.openMissions) do if aMission ~= theMission then table.insert(newMissions, aMission) else +-- csarManager.invokeRemovedMissionCallbacks(theMission) + if pickup then + csarManager.invokePickUpCallbacks(theMission) + end end end csarManager.openMissions = newMissions -- this is the new batch @@ -291,15 +335,24 @@ function csarManager.successMission(who, where, theMission) 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, who .. " successfully evacuated " .. theMission.name .. " to " .. where .. "!", 30) -- now call callback for coalition side - -- callback has format callback(coalition, success true/false, numberSaved, descriptionText) - csarManager.invokeCallbacks(theMission.side, true, 1, "success") + -- callback has format callback(coalition, success true/false, numberSaved, descriptionText, theMission) + 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 cfxZones.pollFlag(csarManager.csarRedDelivered, "inc", csarManager.configZone) @@ -324,7 +377,6 @@ function csarManager.heloLanded(theUnit) conf.unit = theUnit local theGroup = theUnit:getGroup() conf.id = theGroup:getID() - --conf.id = theUnit:getID() conf.currentState = 0 local thePoint = theUnit:getPoint() local mySide = theUnit:getCoalition() @@ -435,7 +487,7 @@ function csarManager.heloLanded(theUnit) args.unitName = myName timer.scheduleFunction(csarManager.asynchSuccess, args, timer.getTime() + 3) - csarManager.removeMission(theMission) + csarManager.removeMission(theMission, true) -- picked up table.insert(conf.troopsOnBoard, theMission) theMission.group:destroy() -- will shut up radio as well theMission.group = nil @@ -466,7 +518,7 @@ function csarManager.asynchSuccess(args) end function csarManager.asynchSound(args) - trigger.action.outSoundForCoalition(args.mySide, csarManager.actionSound) + trigger.action.outSoundForCoalition(args.mySide, csarManager.pickupSound) end -- -- @@ -663,6 +715,7 @@ function csarManager.openMissionsForSide(theSide) end function csarManager.doListCSARRequests(args) + local now = timer.getTime() local conf = args[1] local param = args[2] local theUnit = conf.unit @@ -675,14 +728,34 @@ function csarManager.doListCSARRequests(args) if #openMissions < 1 then report = report .. "\nNo requests, all crew are safe." else - -- iterate through all troops onboard to get their status + -- iterate through all missions and calc distance for idx, mission in pairs(openMissions) do local d = dcsCommon.distFlat(point, mission.zone.point) * 0.000539957 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 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 - 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 -- leave out vectoring report = report .. "\n".. mission.name .. " ADF " .. mission.freq * 10 .. " kHz - " .. status @@ -707,6 +780,7 @@ function csarManager.doStatusCarrying(args) local conf = args[1] local param = args[2] local theUnit = conf.unit + local now = timer.getTime() -- build status report local report = "\nCrew Rescue Status:\n" @@ -717,7 +791,18 @@ function csarManager.doStatusCarrying(args) for i=1, #conf.troopsOnBoard do local evacMission = conf.troopsOnBoard[i] 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 report = report .. "\n\nTotal added weigth: " .. 10 + #conf.troopsOnBoard * csarManager.pilotWeight .. "kg" @@ -886,16 +971,31 @@ end -- function csarManager.updateCSARMissions() local newMissions = {} + local now = timer.getTime() 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) - -- now check if a timer was running to rescue this group + -- if dead, set stillAlive to false - if stillAlive then + if stillRunning and stillAlive then 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 local msg = aMission.name .. " confirmed KIA, repeat KIA. Abort CSAR." 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 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 -- 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 ---old -- contains per group a player record, use prime unit to access player's unit + for pname, aUnit in pairs(allPlayerUnits) do - if --aUnit:isExist() and - aUnit:inAir() and + if aUnit:inAir() and dcsCommon.isTroopCarrier(aUnit, csarManager.troopCarriers) then -- troop carrier and is flying local uPoint = aUnit:getPoint() @@ -1044,7 +1143,7 @@ function csarManager.update() -- every second end --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 end -- hovered long enough @@ -1209,8 +1308,15 @@ function csarManager.readCSARZone(theZone) if theZone.csarFreq < 0.01 then theZone.csarFreq = nil end theZone.numCrew = 1 theZone.csarMapMarker = nil - theZone.timeLimit = theZone:getNumberFromZoneProperty("timeLimit", 0) - if theZone.timeLimit == 0 then theZone.timeLimit = nil else theZone.timeLimit = timeLimit * 60 end + if theZone:hasProperty("timeLimit") then + 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) @@ -1255,7 +1361,7 @@ function csarManager.readCSARZone(theZone) -- add to list of startable csar if theZone.startCSAR then 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 if deferred and not theZone.startCSAR then @@ -1276,11 +1382,29 @@ end -- 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 -- of people was rescued. 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 @@ -1288,6 +1412,14 @@ function csarManager.installCallback(theCB) table.insert(csarManager.csarCompleteCB, theCB) end +function csarManager.installNewMissionCallback(theCB) + table.insert(csarManager.csarCreatedCB, theCB) +end + +function csarManager.installPickupCallback(theCB) + table.insert(csarManager.csarPickupCB, theCB) +end + function csarManager.readConfigZone() csarManager.name = "csarManagerConfig" -- compat with cfxZones local theZone = cfxZones.getZoneByName("csarManagerConfig") @@ -1331,7 +1463,10 @@ function csarManager.readConfigZone() csarManager.rescueScore = theZone:getNumberFromZoneProperty( "rescueScore", 100) 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.lostSound = theZone:getStringFromZoneProperty("lostSound", csarManager.actionSound) -- add own troop carriers if theZone:hasProperty("troopCarriers") then @@ -1348,6 +1483,7 @@ function csarManager.readConfigZone() csarManager.addPrefix = theZone:getBoolFromZoneProperty("addPrefix", true) + csarManager.maxMissions = theZone:getNumberFromZoneProperty("maxMissions", 15) if csarManager.verbose then trigger.action.outText("+++csar: read config", 30) end @@ -1415,4 +1551,6 @@ end -- 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 --]]-- \ No newline at end of file diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index d6e3b2c..d3dcfc4 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "3.0.1" +dcsCommon.version = "3.0.2" --[[-- VERSION HISTORY 3.0.0 - removed bad bug in stringStartsWith, only relevant if caseSensitive is false - point2text new intsOnly option @@ -8,7 +8,9 @@ dcsCommon.version = "3.0.1" - new pointInDirectionOfPointXYY() - createGroundGroupWithUnits now supports liveries - 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 @@ -1934,7 +1936,7 @@ dcsCommon.version = "3.0.1" function dcsCommon.createStaticObjectForCoalitionAtLocation(theCoalition, loc, name, objType, heading, dead) if not heading then heading = math.random(360) * 3.1415 / 180 end 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 end @@ -1947,8 +1949,8 @@ dcsCommon.version = "3.0.1" theData.x = p.x theData.y = p.z - local theStatic = coalition.addStaticObject(theCoalition, theData) - return theStatic + local theStatic = coalition.addStaticObject(theCoalition, theData) -- warning! coalition is not country + return theStatic, p.x, p.z end @@ -2703,6 +2705,19 @@ function dcsCommon.isTroopCarrier(theUnit, carriers) return dcsCommon.isTroopCarrierType(uType, carriers) 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() local apu = {} diff --git a/modules/heloTroops.lua b/modules/heloTroops.lua index e9dd6df..523d0d1 100644 --- a/modules/heloTroops.lua +++ b/modules/heloTroops.lua @@ -1,5 +1,5 @@ cfxHeloTroops = {} -cfxHeloTroops.version = "3.0.0" +cfxHeloTroops.version = "3.0.2" cfxHeloTroops.verbose = false cfxHeloTroops.autoDrop = true cfxHeloTroops.autoPickup = false @@ -37,6 +37,8 @@ cfxHeloTroops.requestRange = 500 -- meters - harmonized spawning invocations across cloners and spawners - dmlZones - 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 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" + commandTxt = "(You are carrying " .. conf.troopsOnBoardNum .. " Assault Troops. Land to deploy them)" end local theCommand = missionCommands.addCommandForGroup( conf.id, @@ -887,7 +889,7 @@ function cfxHeloTroops.readConfigZone() if theZone:hasProperty("legalTroops") then local theTypesString = theZone:getStringFromZoneProperty("legalTroops", "") - local unitTypes = dcsCommon.splitString(aSpawner.types, ",") + local unitTypes = dcsCommon.splitString(theTypesString, ",") 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 diff --git a/modules/messenger.lua b/modules/messenger.lua index 0761630..d1a2138 100644 --- a/modules/messenger.lua +++ b/modules/messenger.lua @@ -1,5 +1,5 @@ messenger = {} -messenger.version = "3.0.0" +messenger.version = "3.1.0" messenger.verbose = false messenger.requiredLibs = { "dcsCommon", -- always @@ -11,6 +11,8 @@ messenger.messengers = {} 2.3.0 - cfxZones OOP switch 2.3.1 - triggering message AFTER the on/off switches are tested 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) @@ -166,18 +168,12 @@ end -- reat attributes -- function messenger.createMessengerWithZone(theZone) - -- start val - a range - local aMessage = theZone:getStringFromZoneProperty("message", "") - theZone.message = aMessage -- refactoring: messenger.preProcMessage(aMessage, theZone) removed - + theZone.message = aMessage theZone.spaceBefore = theZone:getBoolFromZoneProperty("spaceBefore", false) theZone.spaceAfter = theZone:getBoolFromZoneProperty("spaceAfter", false) - theZone.soundFile = theZone:getStringFromZoneProperty("soundFile", "") - theZone.clearScreen = theZone:getBoolFromZoneProperty("clearScreen", false) - theZone.duration = theZone:getNumberFromZoneProperty("duration", 30) if theZone:hasProperty("messageDuration") then theZone.duration = theZone:getNumberFromZoneProperty( "messageDuration", 30) @@ -188,28 +184,8 @@ function messenger.createMessengerWithZone(theZone) if theZone:hasProperty("msgTriggerMethod") then theZone.msgTriggerMethod = theZone:getStringFromZoneProperty("msgTriggerMethod", "change") end - - if theZone:hasProperty("f?") then - 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.triggerMessagerFlag = theZone:getStringFromZoneProperty( "messenger?", "none") + theZone.lastMessageTriggerValue = theZone:getFlagValue(theZone.triggerMessagerFlag) -- save last value theZone.messageOff = theZone:getBoolFromZoneProperty("mute", false) --false if theZone:hasProperty("messageMute") then @@ -235,15 +211,42 @@ function messenger.createMessengerWithZone(theZone) end if theZone:hasProperty("group") then - theZone.msgGroup = theZone:getStringFromZoneProperty("group", "") + -- now supporting multiple groups + local theGroups = {} + local rawGroups = theZone:getStringFromZoneProperty("group", "") + 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 - theZone.msgGroup = theZone:getStringFromZoneProperty("msgGroup", "") + local rawGroups = theZone:getStringFromZoneProperty("msgGroup", "") + if dcsCommon.containsString(rawGroups, ",") then + theGroups = dcsCommon.splitString(rawGroups, ",") + theGroups = dcsCommon.trimArray(theGroups) + theZone.msgGroup = theGroups + else + theZone.msgGroup = {rawGroups,} + end end - if theZone:hasProperty("unit") then - theZone.msgUnit = theZone:getStringFromZoneProperty("unit", "") - elseif theZone:hasProperty("msgUnit") then - theZone.msgUnit = theZone:getStringFromZoneProperty("msgUnit", "") + if theZone:hasProperty("unit") or theZone:hasProperty("msgUnit") then + local rawUnits = "err" + if theZone:hasProperty("unit") then + rawUnits = theZone:getStringFromZoneProperty("unit", "") + else + rawUnits = theZone:getStringFromZoneProperty("msgUnit", "") + end + if dcsCommon.containsString(rawUnits, ",") then + local theUnits = dcsCommon.splitString(rawUnits, ",") + theUnits = dcsCommon.trimArray(theUnits) + theZone.msgUnit = theUnits + else + theZone.msgUnit = {rawUnits,} + end end if (theZone.msgGroup and theZone.msgUnit) or @@ -337,24 +340,28 @@ function messenger.isTriggered(theZone) end trigger.action.outSoundForCoalition(theZone.msgCoalition, fileName) elseif theZone.msgGroup then - local theGroup = Group.getByName(theZone.msgGroup) - if theGroup and Group.isExist(theGroup) then - local ID = theGroup:getID() - msg = messenger.dynamicGroupProcessing(msg, theZone, theGroup) - if #msg > 0 or theZone.clearScreen then - trigger.action.outTextForGroup(ID, msg, theZone.duration, theZone.clearScreen) + for idx, aGroupName in pairs(theZone.msgGroup) do + local theGroup = Group.getByName(aGroupName) + if theGroup and Group.isExist(theGroup) then + local ID = theGroup:getID() + msg = messenger.dynamicGroupProcessing(msg, theZone, theGroup) + if #msg > 0 or theZone.clearScreen then + trigger.action.outTextForGroup(ID, msg, theZone.duration, theZone.clearScreen) + end + trigger.action.outSoundForGroup(ID, fileName) end - trigger.action.outSoundForGroup(ID, fileName) end elseif theZone.msgUnit then - local theUnit = Unit.getByName(theZone.msgUnit) - if theUnit and Unit.isExist(theUnit) then - local ID = theUnit:getID() - msg = messenger.dynamicUnitProcessing(msg, theZone, theUnit) - if #msg > 0 or theZone.clearScreen then - trigger.action.outTextForUnit(ID, msg, theZone.duration, theZone.clearScreen) - end - trigger.action.outSoundForUnit(ID, fileName) + for idx, aUnitName in pairs (theZone.msgUnit) do + local theUnit = Unit.getByName(aUnitName) + if theUnit and Unit.isExist(theUnit) then + local ID = theUnit:getID() + msg = messenger.dynamicUnitProcessing(msg, theZone, theUnit) + if #msg > 0 or theZone.clearScreen then + trigger.action.outTextForUnit(ID, msg, theZone.duration, theZone.clearScreen) + end + trigger.action.outSoundForUnit(ID, fileName) + end end elseif theZone:getLinkedUnit() then -- this only works if the zone is linked to a unit diff --git a/modules/names.lua b/modules/names.lua new file mode 100644 index 0000000..f8e113d --- /dev/null +++ b/modules/names.lua @@ -0,0 +1,1234 @@ +names = {} +names.version = "1.0.0" +-- simply 600 common first and 600 common last names as +-- arrays to generate a LOT (360'000)) of unique names +names.taken = {} +names.firstNames = { + "John", + "William", + "James", + "Charles", + "George", + "Frank", + "Joseph", + "Thomas", + "Henry", + "Robert", + "Edward", + "Harry", + "Walter", + "Arthur", + "Fred", + "Albert", + "Samuel", + "David", + "Louis", + "Joe", + "Charlie", + "Clarence", + "Richard", + "Andrew", + "Daniel", + "Ernest", + "Will", + "Jesse", + "Oscar", + "Lewis", + "Peter", + "Benjamin", + "Frederick", + "Willie", + "Alfred", + "Sam", + "Roy", + "Herbert", + "Jacob", + "Tom", + "Elmer", + "Carl", + "Lee", + "Howard", + "Martin", + "Michael", + "Bert", + "Herman", + "Jim", + "Francis", + "Harvey", + "Earl", + "Eugene", + "Ralph", + "Ed", + "Claude", + "Edwin", + "Ben", + "Charley", + "Paul", + "Edgar", + "Isaac", + "Otto", + "Luther", + "Lawrence", + "Ira", + "Patrick", + "Guy", + "Oliver", + "Theodore", + "Hugh", + "Clyde", + "Alexander", + "August", + "Floyd", + "Homer", + "Jack", + "Leonard", + "Horace", + "Marion", + "Philip", + "Allen", + "Archie", + "Stephen", + "Chester", + "Willis", + "Raymond", + "Rufus", + "Warren", + "Jessie", + "Milton", + "Alex", + "Leo", + "Julius", + "Ray", + "Sidney", + "Bernard", + "Dan", + "Jerry", + "Calvin", + "Perry", + "Dave", + "Anthony", + "Eddie", + "Amos", + "Dennis", + "Clifford", + "Leroy", + "Wesley", + "Alonzo", + "Garfield", + "Franklin", + "Emil", + "Leon", + "Nathan", + "Harold", + "Matthew", + "Levi", + "Moses", + "Everett", + "Lester", + "Winfield", + "Adam", + "Lloyd", + "Mack", + "Fredrick", + "Jay", + "Jess", + "Melvin", + "Noah", + "Aaron", + "Alvin", + "Norman", + "Gilbert", + "Elijah", + "Victor", + "Gus", + "Nelson", + "Jasper", + "Silas", + "Christopher", + "Jake", + "Mike", + "Percy", + "Adolph", + "Maurice", + "Cornelius", + "Felix", + "Reuben", + "Wallace", + "Claud", + "Roscoe", + "Sylvester", + "Earnest", + "Hiram", + "Otis", + "Simon", + "Willard", + "Irvin", + "Mark", + "Jose", + "Wilbur", + "Abraham", + "Virgil", + "Clinton", + "Elbert", + "Leslie", + "Marshall", + "Owen", + "Wiley", + "Anton", + "Morris", + "Manuel", + "Phillip", + "Augustus", + "Emmett", + "Eli", + "Nicholas", + "Wilson", + "Alva", + "Harley", + "Newton", + "Timothy", + "Marvin", + "Ross", + "Curtis", + "Edmund", + "Jeff", + "Elias", + "Harrison", + "Stanley", + "Columbus", + "Lon", + "Ora", + "Ollie", + "Russell", + "Pearl", + "Solomon", + "Arch", + "Asa", + "Clayton", + "Enoch", + "Irving", + "Mathew", + "Nathaniel", + "Scott", + "Hubert", + "Lemuel", + "Andy", + "Ellis", + "Emanuel", + "Joshua", + "Millard", + "Vernon", + "Wade", + "Cyrus", + "Miles", + "Rudolph", + "Sherman", + "Austin", + "Bill", + "Chas", + "Lonnie", + "Monroe", + "Byron", + "Edd", + "Emery", + "Grant", + "Jerome", + "Max", + "Mose", + "Steve", + "Gordon", + "Abe", + "Pete", + "Chris", + "Clark", + "Gustave", + "Orville", + "Lorenzo", + "Bruce", + "Marcus", + "Preston", + "Bob", + "Dock", + "Donald", + "Jackson", + "Cecil", + "Barney", + "Delbert", + "Edmond", + "Anderson", + "Christian", + "Glenn", + "Jefferson", + "Luke", + "Neal", + "Burt", + "Ike", + "Myron", + "Tony", + "Conrad", + "Joel", + "Matt", + "Riley", + "Vincent", + "Emory", + "Isaiah", + "Nick", + "Ezra", + "Green", + "Juan", + "Clifton", + "Lucius", + "Porter", + "Arnold", + "Bud", + "Jeremiah", + "Taylor", + "Forrest", + "Roland", + "Spencer", + "Burton", + "Don", + "Emmet", + "Gustav", + "Louie", + "Morgan", + "Ned", + "Van", + "Ambrose", + "Chauncey", + "Elisha", + "Ferdinand", + "General", + "Julian", + "Kenneth", + "Mitchell", + "Allie", + "Josh", + "Judson", + "Lyman", + "Napoleon", + "Pedro", + "Berry", + "Dewitt", + "Ervin", + "Forest", + "Lynn", + "Pink", + "Ruben", + "Sanford", + "Ward", + "Douglas", + "Ole", + "Omer", + "Ulysses", + "Walker", + "Wilbert", + "Adelbert", + "Benjiman", + "Ivan", + "Jonas", + "Major", + "Abner", + "Archibald", + "Caleb", + "Clint", + "Dudley", + "Granville", + "King", + "Mary", + "Merton", + "Antonio", + "Bennie", + "Carroll", + "Freeman", + "Josiah", + "Milo", + "Royal", + "Dick", + "Earle", + "Elza", + "Emerson", + "Fletcher", + "Judge", + "Laurence", + "Neil", + "Roger", + "Seth", + "Glen", + "Hugo", + "Jimmie", + "Johnnie", + "Washington", + "Elwood", + "Gust", + "Harmon", + "Jordan", + "Simeon", + "Wayne", + "Wilber", + "Clem", + "Evan", + "Frederic", + "Irwin", + "Junius", + "Lafayette", + "Loren", + "Madison", + "Mason", + "Orval", + "Abram", + "Aubrey", + "Elliott", + "Hans", + "Karl", + "Minor", + "Wash", + "Wilfred", + "Allan", + "Alphonse", + "Dallas", + "Dee", + "Isiah", + "Jason", + "Johnny", + "Lawson", + "Lew", + "Micheal", + "Orin", + "Addison", + "Cal", + "Erastus", + "Francisco", + "Hardy", + "Lucien", + "Randolph", + "Stewart", + "Vern", + "Wilmer", + "Zack", + "Adrian", + "Alvah", + "Bertram", + "Clay", + "Ephraim", + "Fritz", + "Giles", + "Grover", + "Harris", + "Isom", + "Jesus", + "Johnie", + "Jonathan", + "Lucian", + "Malcolm", + "Merritt", + "Otho", + "Perley", + "Rolla", + "Sandy", + "Tomas", + "Wilford", + "Adolphus", + "Angus", + "Arther", + "Carlos", + "Cary", + "Cassius", + "Davis", + "Hamilton", + "Harve", + "Israel", + "Leander", + "Melville", + "Merle", + "Murray", + "Pleasant", + "Sterling", + "Steven", + "Axel", + "Boyd", + "Bryant", + "Clement", + "Erwin", + "Ezekiel", + "Foster", + "Frances", + "Geo", + "Houston", + "Issac", + "Jules", + "Larkin", + "Mat", + "Morton", + "Orlando", + "Pierce", + "Prince", + "Rollie", + "Rollin", + "Sim", + "Stuart", + "Wilburn", + "Bennett", + "Casper", + "Christ", + "Dell", + "Egbert", + "Elmo", + "Fay", + "Gabriel", + "Hector", + "Horatio", + "Lige", + "Saul", + "Smith", + "Squire", + "Tobe", + "Tommie", + "Wyatt", + "Alford", + "Alma", + "Alton", + "Andres", + "Burl", + "Cicero", + "Dean", + "Dorsey", + "Enos", + "Howell", + "Lou", + "Loyd", + "Mahlon", + "Nat", + "Omar", + "Oran", + "Parker", + "Raleigh", + "Reginald", + "Rubin", + "Seymour", + "Wm", + "Young", + "Benjamine", + "Carey", + "Carlton", + "Eldridge", + "Elzie", + "Garrett", + "Isham", + "Johnson", + "Larry", + "Logan", + "Merrill", + "Mont", + "Oren", + "Pierre", + "Rex", + "Rodney", + "Ted", + "Webster", + "West", + "Wheeler", + "Willam", + "Al", + "Aloysius", + "Alvie", + "Anna", + "Art", + "Augustine", + "Bailey", + "Benjaman", + "Beverly", + "Bishop", + "Clair", + "Cloyd", + "Coleman", + "Dana", + "Duncan", + "Dwight", + "Emile", + "Evert", + "Henderson", + "Hunter", + "Jean", + "Lem", + "Luis", + "Mathias", + "Maynard", + "Miguel", + "Mortimer", + "Nels", + "Norris", + "Pat", + "Phil", + "Rush", + "Santiago", + "Sol", + "Sydney", + "Thaddeus", + "Thornton", + "Tim", + "Travis", + "Truman", + "Watson", + "Webb", + "Wellington", + "Winfred", + "Wylie", + "Alec", + "Basil", + "Baxter", + "Bertrand", + "Buford", + "Burr", + "Cleveland", + "Colonel", + "Dempsey", + "Early", + "Ellsworth", + "Fate", + "Finley", + "Gabe", + "Garland", + "Gerald", + "Herschel", + "Hezekiah", + "Justus", + "Lindsey", + "Marcellus", + "Olaf", + "Olin", + "Pablo", + "Rolland", + "Turner", + "Verne", + "Volney", + "Williams", + "Almon", +} + +names.lastNames = { + "Smith", + "Johnson", + "Williams", + "Brown", + "Jones", + "Miller", + "Davis", + "Garcia", + "Rodriguez", + "Wilson", + "Martinez", + "Anderson", + "Taylor", + "Thomas", + "Hernandez", + "Moore", + "Martin", + "Jackson", + "Thompson", + "White", + "Lopez", + "Lee", + "Gonzalez", + "Harris", + "Clark", + "Lewis", + "Robinson", + "Walker", + "Perez", + "Hall", + "Young", + "Allen", + "Sanchez", + "Wright", + "King", + "Scott", + "Green", + "Baker", + "Adams", + "Nelson", + "Hill", + "Ramirez", + "Campbell", + "Mitchell", + "Roberts", + "Carter", + "Phillips", + "Evans", + "Turner", + "Torres", + "Parker", + "Collins", + "Edwards", + "Stewart", + "Flores", + "Morris", + "Nguyen", + "Murphy", + "Rivera", + "Cook", + "Rogers", + "Morgan", + "Peterson", + "Cooper", + "Reed", + "Bailey", + "Bell", + "Gomez", + "Kelly", + "Howard", + "Ward", + "Cox", + "Diaz", + "Richardson", + "Wood", + "Watson", + "Brooks", + "Bennett", + "Gray", + "James", + "Reyes", + "Cruz", + "Hughes", + "Price", + "Myers", + "Long", + "Foster", + "Sanders", + "Ross", + "Morales", + "Powell", + "Sullivan", + "Russell", + "Ortiz", + "Jenkins", + "Gutierrez", + "Perry", + "Butler", + "Barnes", + "Fisher", + "Henderson", + "Coleman", + "Simmons", + "Patterson", + "Jordan", + "Reynolds", + "Hamilton", + "Graham", + "Kim", + "Gonzales", + "Alexander", + "Ramos", + "Wallace", + "Griffin", + "West", + "Cole", + "Hayes", + "Chavez", + "Gibson", + "Bryant", + "Ellis", + "Stevens", + "Murray", + "Ford", + "Marshall", + "Owens", + "Mcdonald", + "Harrison", + "Ruiz", + "Kennedy", + "Wells", + "Alvarez", + "Woods", + "Mendoza", + "Castillo", + "Olson", + "Webb", + "Washington", + "Tucker", + "Freeman", + "Burns", + "Henry", + "Vasquez", + "Snyder", + "Simpson", + "Crawford", + "Jimenez", + "Porter", + "Mason", + "Shaw", + "Gordon", + "Wagner", + "Hunter", + "Romero", + "Hicks", + "Dixon", + "Hunt", + "Palmer", + "Robertson", + "Black", + "Holmes", + "Stone", + "Meyer", + "Boyd", + "Mills", + "Warren", + "Fox", + "Rose", + "Rice", + "Moreno", + "Schmidt", + "Patel", + "Ferguson", + "Nichols", + "Herrera", + "Medina", + "Ryan", + "Fernandez", + "Weaver", + "Daniels", + "Stephens", + "Gardner", + "Payne", + "Kelley", + "Dunn", + "Pierce", + "Arnold", + "Tran", + "Spencer", + "Peters", + "Hawkins", + "Grant", + "Hansen", + "Castro", + "Hoffman", + "Hart", + "Elliott", + "Cunningham", + "Knight", + "Bradley", + "Carroll", + "Hudson", + "Duncan", + "Armstrong", + "Berry", + "Andrews", + "Johnston", + "Ray", + "Lane", + "Riley", + "Carpenter", + "Perkins", + "Aguilar", + "Silva", + "Richards", + "Willis", + "Matthews", + "Chapman", + "Lawrence", + "Garza", + "Vargas", + "Watkins", + "Wheeler", + "Larson", + "Carlson", + "Harper", + "George", + "Greene", + "Burke", + "Guzman", + "Morrison", + "Munoz", + "Jacobs", + "Obrien", + "Lawson", + "Franklin", + "Lynch", + "Bishop", + "Carr", + "Salazar", + "Austin", + "Mendez", + "Gilbert", + "Jensen", + "Williamson", + "Montgomery", + "Harvey", + "Oliver", + "Howell", + "Dean", + "Hanson", + "Weber", + "Garrett", + "Sims", + "Burton", + "Fuller", + "Soto", + "Mccoy", + "Welch", + "Chen", + "Schultz", + "Walters", + "Reid", + "Fields", + "Walsh", + "Little", + "Fowler", + "Bowman", + "Davidson", + "May", + "Day", + "Schneider", + "Newman", + "Brewer", + "Lucas", + "Holland", + "Wong", + "Banks", + "Santos", + "Curtis", + "Pearson", + "Delgado", + "Valdez", + "Pena", + "Rios", + "Douglas", + "Sandoval", + "Barrett", + "Hopkins", + "Keller", + "Guerrero", + "Stanley", + "Bates", + "Alvarado", + "Beck", + "Ortega", + "Wade", + "Estrada", + "Contreras", + "Barnett", + "Caldwell", + "Santiago", + "Lambert", + "Powers", + "Chambers", + "Nunez", + "Craig", + "Leonard", + "Lowe", + "Rhodes", + "Byrd", + "Gregory", + "Shelton", + "Frazier", + "Becker", + "Maldonado", + "Fleming", + "Vega", + "Sutton", + "Cohen", + "Jennings", + "Parks", + "Mcdaniel", + "Watts", + "Barker", + "Norris", + "Vaughn", + "Vazquez", + "Holt", + "Schwartz", + "Steele", + "Benson", + "Neal", + "Dominguez", + "Horton", + "Terry", + "Wolfe", + "Hale", + "Lyons", + "Graves", + "Haynes", + "Miles", + "Park", + "Warner", + "Padilla", + "Bush", + "Thornton", + "Mccarthy", + "Mann", + "Zimmerman", + "Erickson", + "Fletcher", + "Mckinney", + "Page", + "Dawson", + "Joseph", + "Marquez", + "Reeves", + "Klein", + "Espinoza", + "Baldwin", + "Moran", + "Love", + "Robbins", + "Higgins", + "Ball", + "Cortez", + "Le", + "Griffith", + "Bowen", + "Sharp", + "Cummings", + "Ramsey", + "Hardy", + "Swanson", + "Barber", + "Acosta", + "Luna", + "Chandler", + "Blair", + "Daniel", + "Cross", + "Simon", + "Dennis", + "Oconnor", + "Quinn", + "Gross", + "Navarro", + "Moss", + "Fitzgerald", + "Doyle", + "Mclaughlin", + "Rojas", + "Rodgers", + "Stevenson", + "Singh", + "Yang", + "Figueroa", + "Harmon", + "Newton", + "Paul", + "Manning", + "Garner", + "Mcgee", + "Reese", + "Francis", + "Burgess", + "Adkins", + "Goodman", + "Curry", + "Brady", + "Christensen", + "Potter", + "Walton", + "Goodwin", + "Mullins", + "Molina", + "Webster", + "Fischer", + "Campos", + "Avila", + "Sherman", + "Todd", + "Chang", + "Blake", + "Malone", + "Wolf", + "Hodges", + "Juarez", + "Gill", + "Farmer", + "Hines", + "Gallagher", + "Duran", + "Hubbard", + "Cannon", + "Miranda", + "Wang", + "Saunders", + "Tate", + "Mack", + "Hammond", + "Carrillo", + "Townsend", + "Wise", + "Ingram", + "Barton", + "Mejia", + "Ayala", + "Schroeder", + "Hampton", + "Rowe", + "Parsons", + "Frank", + "Waters", + "Strickland", + "Osborne", + "Maxwell", + "Chan", + "Deleon", + "Norman", + "Harrington", + "Casey", + "Patton", + "Logan", + "Bowers", + "Mueller", + "Glover", + "Floyd", + "Hartman", + "Buchanan", + "Cobb", + "French", + "Kramer", + "Mccormick", + "Clarke", + "Tyler", + "Gibbs", + "Moody", + "Conner", + "Sparks", + "Mcguire", + "Leon", + "Bauer", + "Norton", + "Pope", + "Flynn", + "Hogan", + "Robles", + "Salinas", + "Yates", + "Lindsey", + "Lloyd", + "Marsh", + "Mcbride", + "Owen", + "Solis", + "Pham", + "Lang", + "Pratt", + "Lara", + "Brock", + "Ballard", + "Trujillo", + "Shaffer", + "Drake", + "Roman", + "Aguirre", + "Morton", + "Stokes", + "Lamb", + "Pacheco", + "Patrick", + "Cochran", + "Shepherd", + "Cain", + "Burnett", + "Hess", + "Li", + "Cervantes", + "Olsen", + "Briggs", + "Ochoa", + "Cabrera", + "Velasquez", + "Montoya", + "Roth", + "Meyers", + "Cardenas", + "Fuentes", + "Weiss", + "Hoover", + "Wilkins", + "Nicholson", + "Underwood", + "Short", + "Carson", + "Morrow", + "Colon", + "Holloway", + "Summers", + "Bryan", + "Petersen", + "Mckenzie", + "Serrano", + "Wilcox", + "Carey", + "Clayton", + "Poole", + "Calderon", + "Gallegos", + "Greer", + "Rivas", + "Guerra", + "Decker", + "Collier", + "Wall", + "Whitaker", + "Bass", + "Flowers", + "Davenport", + "Conley", + "Houston", + "Huff", + "Copeland", + "Hood", + "Monroe", + "Massey", + "Roberson", + "Combs", + "Franco", + "Larsen", + "Pittman", + "Randall", + "Skinner", + "Wilkinson", + "Kirby", + "Cameron", + "Bridges", + "Anthony", + "Richard", + "Kirk", + "Bruce", + "Singleton", + "Mathis", + "Bradford", + "Boone", + "Abbott", + "Charles", + "Allison", + "Sweeney", + "Atkinson", + "Horn", + "Jefferson", + "Rosales", + "York", + "Christian", + "Phelps", + "Farrell", + "Castaneda", +} + +function names.randomFirstName() + local num = #names.firstNames + local rnd = math.random(num) + return names.firstNames[rnd] +end + + +function names.randomLastName() + local num = #names.lastNames + local rnd = math.random(num) + return names.lastNames[rnd] +end + +function names.uniqueFullName() + local fn + repeat + fn = names.randomFirstName() .. " " .. names.randomLastName() + until not names.taken[fn] + names.taken[fn] = true + return fn +end + +trigger.action.outText("cfx names version " .. names.version .. " loaded.", 30) \ No newline at end of file diff --git a/modules/ownAll.lua b/modules/ownAll.lua index 51bd25c..f58ac61 100644 --- a/modules/ownAll.lua +++ b/modules/ownAll.lua @@ -136,7 +136,7 @@ end function ownAll.start() -- lib check if not dcsCommon.libCheck then - trigger.action.outText("cfx ownAlll requires dcsCommon", 30) + trigger.action.outText("cfx ownAll requires dcsCommon", 30) return false end if not dcsCommon.libCheck("cfx ownAll", ownAll.requiredLibs) then diff --git a/modules/persistence.lua b/modules/persistence.lua index f509754..600118a 100644 --- a/modules/persistence.lua +++ b/modules/persistence.lua @@ -30,7 +30,6 @@ persistence.requiredLibs = { persistence.flagsToSave = {} -- simple table persistence.callbacks = {} -- cbblocks, dictionary by name - -- -- modules register here -- @@ -76,7 +75,6 @@ function persistence.getSavedDataForModule(name) return persistence.missionData[name] -- simply get the modules data block end - -- -- Shared Data API -- @@ -113,7 +111,6 @@ function persistence.isDir(path) -- check if path is a directory return success end - -- -- Main save meths -- @@ -189,7 +186,6 @@ function persistence.saveTable(theTable, fileName, shared, append) return true end - function persistence.loadText(fileName) -- load file as text if not persistence.active 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 end - - -- -- Data Load on Start -- @@ -401,7 +395,6 @@ end -- -- config & start -- - function persistence.collectFlagsFromZone(theZone) local theFlags = theZone:getStringFromZoneProperty("saveFlags", "*dummy") persistence.registerFlagsToSave(theFlags, theZone) @@ -409,7 +402,7 @@ end function persistence.readConfigZone() 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 end diff --git a/modules/radioMenus.lua b/modules/radioMenus.lua index dd9cafd..65f89e0 100644 --- a/modules/radioMenus.lua +++ b/modules/radioMenus.lua @@ -1,5 +1,5 @@ radioMenu = {} -radioMenu.version = "2.2.0" +radioMenu.version = "2.2.1" radioMenu.verbose = false radioMenu.ups = 1 radioMenu.requiredLibs = { @@ -18,6 +18,7 @@ radioMenu.menus = {} full wildcard support for ack and cooldown 2.1.1 - outMessage now works correctly 2.2.0 - clean-up + 2.2.1 - corrected ackD --]]-- function radioMenu.addRadioMenu(theZone) @@ -299,7 +300,7 @@ function radioMenu.createRadioMenuWithZone(theZone) theZone.outValD = theZone:getStringFromZoneProperty("valD", 1) end if theZone:hasProperty("ackD") then - theZone.ackC = theZone:getStringFromZoneProperty("ackD", "Acknowledged: D") + theZone.ackD = theZone:getStringFromZoneProperty("ackD", "Acknowledged: D") end if theZone:hasProperty("removeMenu?") then diff --git a/modules/scribe.lua b/modules/scribe.lua new file mode 100644 index 0000000..125ad2a --- /dev/null +++ b/modules/scribe.lua @@ -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 diff --git a/tutorial & demo missions/demo - On the Record.miz b/tutorial & demo missions/demo - On the Record.miz new file mode 100644 index 0000000..223b30e Binary files /dev/null and b/tutorial & demo missions/demo - On the Record.miz differ