diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 1d686ba..3196ab9 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 b84007a..e8648a6 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/autoCSAR.lua b/modules/autoCSAR.lua index 04ef3d5..847ecb7 100644 --- a/modules/autoCSAR.lua +++ b/modules/autoCSAR.lua @@ -27,7 +27,7 @@ function autoCSAR.createNewCSAR(theUnit) -- unit has no group local coa = theUnit:getCoalition() if coa == 0 then -- neutral - trigger.action.outText("Neutal Pilot made it safely to ground.", 30) + trigger.action.outText("Neutral Pilot made it safely to ground.", 30) return end if coa == 1 and not autoCSAR.redCSAR then diff --git a/modules/cfxSSBClient.lua b/modules/cfxSSBClient.lua index cbd84e0..644ac0b 100644 --- a/modules/cfxSSBClient.lua +++ b/modules/cfxSSBClient.lua @@ -1,5 +1,5 @@ cfxSSBClient = {} -cfxSSBClient.version = "2.1.0" +cfxSSBClient.version = "3.0.0" cfxSSBClient.verbose = false cfxSSBClient.singleUse = false -- set to true to block crashed planes -- NOTE: singleUse (true) requires SSB to disable immediate respawn after kick @@ -19,7 +19,7 @@ Version History 1.1.1 - performance tuning. only read player groups once - and remove in-air-start groups from scan. this requires - ssb (server) be not modified - 1.2.0 - API to close airfields: invoke openAirfieldNamed() + 1.2.0 - API to close airfields: invoke openAirFieldNamed() and closeAirfieldNamed() with name as string (exact match required) to block an airfield for any player aircraft. Works for FARPS as well @@ -42,6 +42,12 @@ Version History in onEvent 2.1.0 - slotState - persistence + 3.0.0 - closing an airfield will not kick players who are active + - much better verbosity + - open? + - close? + - also persists closed airfield list + WHAT IT IS @@ -92,8 +98,59 @@ cfxSSBClient.closedAirfields = {} -- list that closes airfields for any aircraft cfxSSBClient.playerPlanes = {} -- names of units that a player is flying cfxSSBClient.crashedGroups = {} -- names of groups to block after crash of their player-flown plane cfxSSBClient.slotState = {} -- keeps a record of which slot has which value. For persistence and debugging +cfxSSBClient.occupiedUnits = {} -- by unit name if occupied to prevent kicking. clears after crash or leaving plane +-- will not be persisted because on start all planes are empty +-- dml zone interface for open/close interface +cfxSSBClient.clientZones = {} +function cfxSSBClient.addClientZone(theZone) + table.insert(cfxSSBClient.clientZones, theZone) +end + +function cfxSSBClient.getClientZoneByName(aName) + for idx, aZone in pairs(cfxSSBClient.clientZones) do + if aName == aZone.name then return aZone end + end + if cfxSSBClient.verbose then + trigger.action.outText("+++ssbc: no client zone with name <" .. aName ..">", 30) + end + + return nil +end + +-- +-- read client zones +-- +function cfxSSBClient.createClientZone(theZone) + local thePoint = cfxZones.getPoint(theZone) + local theAF = cfxSSBClient.getClosestAirbaseTo(thePoint) + local afName = theAF:getName() + if cfxSSBClient.verbose or theZone.verbose then + trigger.action.outText("+++ssbc: zone <" .. theZone.name .. "> linked to AF/FARP <" .. afName .. ">", 30) + end + theZone.afName = afName + theZone.ssbTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "ssbTriggerMethod", "change") + + if cfxZones.hasProperty(theZone, "open?") then + theZone.ssbOpen = cfxZones.getStringFromZoneProperty(theZone, "open?", "none") + theZone.lastSsbOpen = cfxZones.getFlagValue(theZone.ssbOpen, theZone) + end + + if cfxZones.hasProperty(theZone, "close?") then + theZone.ssbClose = cfxZones.getStringFromZoneProperty(theZone, "close?", "none") + theZone.lastSsbClose = cfxZones.getFlagValue(theZone.ssbClose, theZone) + end + + theZone.ssbOpenOnStart = cfxZones.getBoolFromZoneProperty(theZone, "openOnStart", true) + if not theZone.ssbOpenOnStart then + cfxSSBClient.closeAirfieldNamed(theZone.afName) + end +end + +-- +-- Open / Close Airfield API +-- function cfxSSBClient.closeAirfieldNamed(name) if not name then return end cfxSSBClient.closedAirfields[name] = true @@ -155,6 +212,25 @@ function cfxSSBClient.setSlotAccessForGroup(theGroup) if not theGroup then return end -- WARNING: theGroup is cfxGroup record local theName = theGroup.name + + -- we now check if any plane of that group is still + -- existing and in the air. if so, we skip this check + -- to prevent players being kicked for losing their + -- originating airfield + + -- we now iterate all playerUnits in theGroup. + -- theGroup is cfxGroup + for idx, playerData in pairs (theGroup.playerUnits) do + local uName = playerData.name + if cfxSSBClient.occupiedUnits[uName] then + if cfxSSBClient.verbose then + trigger.action.outText("+++ssbc: unit <" .. uName .. "> of group <" .. theName .. "> is occupied, no airfield check", 30) + end + return + end + end + + -- when we get here, no unit in the entire group is occupied local theMatchingAirfield = theGroup.airfield -- airfield was attached at startup to group if cfxSSBClient.singleUse and cfxSSBClient.crashedGroups[theName] then @@ -194,11 +270,17 @@ function cfxSSBClient.setSlotAccessForGroup(theGroup) end end -- set the ssb flag for this group so the server can see it + if cfxSSBClient.verbose then + local lastState = trigger.misc.getUserFlag(theName) + if lastState ~= blockState then + trigger.action.outText("+++ssbc: <" .. theName .. "> changes from <" .. lastState .. "> to <" .. blockState .. ">", 30) + trigger.action.outText("+++SSB: group ".. theName .. ": " .. comment, 30) + end + end trigger.action.setUserFlag(theName, blockState) cfxSSBClient.slotState[theName] = blockState - if cfxSSBClient.verbose then - trigger.action.outText("+++SSB: group ".. theName .. ": " .. comment, 30) - end + --if cfxSSBClient.verbose then + --end else if cfxSSBClient.verbose then trigger.action.outText("+++SSB: group ".. theName .. " no bound airfield: available", 30) @@ -206,6 +288,18 @@ function cfxSSBClient.setSlotAccessForGroup(theGroup) end end +function cfxSSBClient.setSlotAccessForUnit(theUnit) -- calls setSlotAccessForGroup + if not theUnit then return end + local theGroup = theUnit:getGroup() + if not theGroup then return end + local gName = theGroup:getName() + if not gName then return end + local pGroup = cfxSSBClient.getPlayerGroupForGroupNamed(gName) + if pGroup then + cfxSSBClient.setSlotAccessForGroup(pGroup) + end +end + function cfxSSBClient.getPlayerGroupForGroupNamed(aName) local pGroups = cfxSSBClient.playerGroups for idx, theGroup in pairs(pGroups) do @@ -217,11 +311,10 @@ end function cfxSSBClient.setSlotAccessByAirfieldOwner() -- get all groups that have a player-controlled aircraft -- now uses cached, reduced set of player planes - local pGroups = cfxSSBClient.playerGroups -- cfxGroups.getPlayerGroup() -- we want the group.name attribute + local pGroups = cfxSSBClient.playerGroups for idx, theGroup in pairs(pGroups) do cfxSSBClient.setSlotAccessForGroup(theGroup) end - end function cfxSSBClient.reOpenSlotForGroupNamed(args) @@ -243,9 +336,6 @@ end function cfxSSBClient:onEvent(event) if event.id == 21 then -- S_EVENT_PLAYER_LEAVE_UNIT - if cfxSSBClient.verbose then - trigger.action.outText("+++SSB: Player leave unit", 30) - end local theUnit = event.initiator if not theUnit then if cfxSSBClient.verbose then @@ -255,30 +345,38 @@ function cfxSSBClient:onEvent(event) end local curH = theUnit:getLife() local maxH = theUnit:getLife0() - if cfxSSBClient.verbose then - trigger.action.outText("+++SSB: Health check: " .. curH .. " of " .. maxH, 30) + local uName = theUnit:getName() + if cfxSSBClient.verbose then + trigger.action.outText("+++SSB: Player leaves unit <" .. uName .. ">", 30) + trigger.action.outText("+++SSB: unit health check: " .. curH .. " of " .. maxH, 30) end + + cfxSSBClient.occupiedUnits[uName] = nil -- forget I was occupied + cfxSSBClient.setSlotAccessForUnit(theUnit) -- prevent re-slotting if airfield lost return end if event.id == 10 then -- S_EVENT_BASE_CAPTURED - if cfxSSBClient.verbose then - trigger.action.outText("+++SSB: CAPTURE EVENT -- RESETTING SLOTS", 30) + if cfxSSBClient.verbose then + local place = event.place + + trigger.action.outText("+++SSB: CAPTURE EVENT: <" .. place:getName() .. "> now owned by <" .. place:getCoalition() .. "> -- RESETTING SLOTS", 30) end cfxSSBClient.setSlotAccessByAirfieldOwner() end -- write down player names and planes - if event.id == 15 then - --trigger.action.outText("+++SSBC:SU: enter event 15", 30) + if event.id == 15 then -- birth if not event.initiator then return end local theUnit = event.initiator -- we know this exists local uName = theUnit:getName() if not uName then return end -- player entered unit? - -- check if this is cloned impostor + -- check if this is a cloned impostor if not theUnit.getPlayerName then - trigger.action.outText("+++SSBC: non-player 'client' " .. uName .. " detected, ignoring.", 30) + if cfxSSBClient.verbose then + trigger.action.outText("+++SSBC: non-player 'client' " .. uName .. " detected, ignoring.", 30) + end return end local playerName = theUnit:getPlayerName() @@ -291,12 +389,22 @@ function cfxSSBClient:onEvent(event) if cfxSSBClient.verbose then trigger.action.outText("+++SSBC:SU: noted " .. playerName .. " piloting player unit " .. uName, 30) end + -- mark it as occupied to player won't get kicked until they + -- leave the unit + cfxSSBClient.occupiedUnits[uName] = playerName return end + if event.id == 5 then -- crash PRE-processing + if not event.initiator then return end + local theUnit = event.initiator + local uName = theUnit:getName() + cfxSSBClient.occupiedUnits[uName] = nil -- no longer occupied + cfxSSBClient.setSlotAccessForUnit(theUnit) -- prevent re-slotting if airfield lost + end if cfxSSBClient.singleUse and event.id == 5 then -- crash - if not event.initiator then return end + --if not event.initiator then return end local theUnit = event.initiator local uName = theUnit:getName() if not uName then return end @@ -321,7 +429,9 @@ function cfxSSBClient:onEvent(event) -- remember this plane to not re-enable if -- airfield changes hands later cfxSSBClient.crashedGroups[gName] = thePilot -- set to crash pilot - trigger.action.outText("+++SSBC:SU: Blocked slot for group <" .. gName .. ">", 30) + if cfxSSBClient.verbose then + trigger.action.outText("+++SSBC:SU: Blocked slot for group <" .. gName .. ">", 30) + end if cfxSSBClient.reUseAfter > 0 then -- schedule re-opening this slot in seconds @@ -340,8 +450,38 @@ function cfxSSBClient.update() -- now establish all slot blocks cfxSSBClient.setSlotAccessByAirfieldOwner() + + -- show occupied planes + if cfxSSBClient.verbose then + for uName, pName in pairs (cfxSSBClient.occupiedUnits) do + trigger.action.outText("+++ssbc: <" .. uName .. "> occupied by <" .. pName .. ">", 30) + end + end end +function cfxSSBClient.dmlUpdate() + -- first, re-schedule me in one second + timer.scheduleFunction(cfxSSBClient.dmlUpdate, {}, timer.getTime() + 1) + + for idx, theZone in pairs (cfxSSBClient.clientZones) do + -- see if we received any signals on out inputs + if theZone.ssbOpen and cfxZones.testZoneFlag(theZone, theZone.ssbOpen, theZone.ssbTriggerMethod, "lastSsbOpen") then + if theZone.verbose then + trigger.action.outText("+++ssbc: <" .. theZone.name .. "> open input triggered for <" .. theZone.afName .. ">", 30) + end + cfxSSBClient.openAirFieldNamed(theZone.afName) + end + + if theZone.ssbClose and cfxZones.testZoneFlag(theZone, theZone.ssbClose, theZone.ssbTriggerMethod, "lastSsbClose") then + if theZone.verbose then + trigger.action.outText("+++ssbc: <" .. theZone.name .. "> close input triggered for <" .. theZone.afName .. ">", 30) + end + cfxSSBClient.closeAirfieldNamed(theZone.afName) + end + end +end + + -- pre-process static player data to minimize -- processor load on checks function cfxSSBClient.processPlayerData() @@ -376,7 +516,7 @@ function cfxSSBClient.processGroupData() theAirfield, delta = cfxSSBClient.getClosestAirbaseTo(playerData.point) local afName = theAirfield:getName() if cfxSSBClient.verbose then - trigger.action.outText("+++SSB: group: " .. theGroup.name .. " closest to AF " .. afName .. ": " .. delta .. "m" , 30) + trigger.action.outText("+++SSB: group: " .. theGroup.name .. " closest to AF " .. afName .. ": " .. math.floor(delta) .. "m" , 30) end if delta > cfxSSBClient.maxAirfieldRange then -- forget airfield @@ -437,8 +577,10 @@ function cfxSSBClient.saveData() local theData = {} local states = dcsCommon.clone(cfxSSBClient.slotState) local crashed = dcsCommon.clone(cfxSSBClient.crashedGroups) + local closed = dcsCommon.clone(cfxSSBClient.closedAirfields) theData.states = states theData.crashed = crashed + theData.closed = closed return theData end @@ -463,12 +605,17 @@ function cfxSSBClient.loadData() trigger.action.outText("SSB: blocked <" .. slot .. "> on load", 30) end end - - cfxSSBClient.crashedGroups = theData.crashed - if not cfxSSBClient.crashedGroups then - cfxSSBClient.crashedGroups = {} - trigger.action.outText("SSBClient: nil crashers on load", 30) - end + if theData.crashed then + cfxSSBClient.crashedGroups = theData.crashed + if not cfxSSBClient.crashedGroups then + cfxSSBClient.crashedGroups = {} + trigger.action.outText("SSBClient: nil crashers on load", 30) + end + end + + if theData.closed then + cfxSSBClient.closedAirfields = theData.closed + end end @@ -495,9 +642,20 @@ function cfxSSBClient.start() -- into cfxSSBClient.playerGroups cfxSSBClient.processPlayerData() + -- process ssbc zones + -- for in-mission DML interface + local attrZones = cfxZones.getZonesWithAttributeNamed("ssbClient") + for k, theZone in pairs(attrZones) do + cfxSSBClient.createClientZone(theZone) -- process attributes + cfxSSBClient.addClientZone(theZone) -- add to list + end + -- install a timed update just to make sure -- and start NOW timer.scheduleFunction(cfxSSBClient.update, {}, timer.getTime() + 1) + + -- start dml update (on a different timer + cfxSSBClient.dmlUpdate() -- now turn on ssb trigger.action.setUserFlag("SSB",100) @@ -515,7 +673,6 @@ function cfxSSBClient.start() -- say hi! trigger.action.outText("cfxSSBClient v".. cfxSSBClient.version .. " running, SBB enabled", 30) - --cfxSSBClient.allYourBase() return true end diff --git a/modules/cfxSmokeZones.lua b/modules/cfxSmokeZones.lua index 13b9a35..03125cc 100644 --- a/modules/cfxSmokeZones.lua +++ b/modules/cfxSmokeZones.lua @@ -1,5 +1,5 @@ cfxSmokeZone = {} -cfxSmokeZone.version = "1.1.0" +cfxSmokeZone.version = "1.1.1" cfxSmokeZone.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -16,16 +16,8 @@ cfxSmokeZone.requiredLibs = { - alphanum DML flag upgrade - random color support 1.1.0 - Watchflag upgrade + 1.1.1 - stopSmoke? input - SMOKE ZONES *** EXTENDS ZONES *** - keeps 'eternal' smoke up for any zone that has the - 'smoke' attribute - - USAGE - add a 'smoke' attribute to the zone. the value of the attribute - defines the color. Valid values are: red, green, blue, white, orange, 0 (results in green smoke), 1 (red smoke), 2 (white), 3 (orange), 4 (blue) - defaults to "green" - altiude is meters above ground height, defaults to 5m --]]-- cfxSmokeZone.smokeZones = {} cfxSmokeZone.updateDelay = 5 * 60 -- every 5 minutes @@ -51,9 +43,7 @@ function cfxSmokeZone.processSmokeZone(aZone) -- f? query flags if cfxZones.hasProperty(aZone, "f?") then aZone.onFlag = cfxZones.getStringFromZoneProperty(aZone, "f?", "*") - end - - if cfxZones.hasProperty(aZone, "startSmoke?") then + elseif cfxZones.hasProperty(aZone, "startSmoke?") then aZone.onFlag = cfxZones.getStringFromZoneProperty(aZone, "startSmoke?", "none") end @@ -61,6 +51,11 @@ function cfxSmokeZone.processSmokeZone(aZone) aZone.onFlagVal = cfxZones.getFlagValue(aZone.onFlag, aZone) -- save last value end + if cfxZones.hasProperty(aZone, "stopSmoke?") then + aZone.smkStopFlag = cfxZones.getStringFromZoneProperty(aZone, "stopSmoke?", "") + aZone.smkLastStopFlag = cfxZones.getFlagValue(aZone.smkStopFlag, aZone) + end + -- watchflags: -- triggerMethod aZone.smokeTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "triggerMethod", "change") @@ -145,16 +140,13 @@ function cfxSmokeZone.checkFlags() -- see if this changed if cfxZones.testZoneFlag(aZone, aZone.onFlag, aZone.smokeTriggerMethod, "onFlagVal") then cfxSmokeZone.startSmoke(aZone) - end ---[[-- - -- old code - local currTriggerVal = cfxZones.getFlagValue(aZone.onFlag, aZone) -- trigger.misc.getUserFlag(aZone.onFlag) - if currTriggerVal ~= aZone.onFlagVal then - -- yupp, trigger start - cfxSmokeZone.startSmoke(aZone) - aZone.onFlagVal = currTriggerVal - end ---]]-- + end + end + + if aZone.smkStopFlag then + if cfxZones.testZoneFlag(aZone, aZone.smkStopFlag, aZone.smokeTriggerMethod, "smkLastStopFlag") then + aZone.paused = true -- will no longer re-smoke on update + end end end end diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index 6acab1c..ea8d1f1 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "2.7.5" +dcsCommon.version = "2.7.6" --[[-- VERSION HISTORY 2.2.6 - compassPositionOfARelativeToB - clockPositionOfARelativeToB @@ -104,6 +104,7 @@ dcsCommon.version = "2.7.5" 2.7.5 - new bitAND32() - new LSR() - new num2bin() + 2.7.6 - new getObjectsForCatAtPointWithRadius() --]]-- @@ -2693,6 +2694,25 @@ function dcsCommon.objectHandler(theObject, theCollector) return true end +function dcsCommon.getObjectsForCatAtPointWithRadius(aCat, thePoint, theRadius) + if not aCat then aCat = Object.Category.UNIT end + local p = {x=thePoint.x, y=thePoint.y, z=thePoint.z} + local collector = {} + + -- now build the search argument + local args = { + id = world.VolumeType.SPHERE, + params = { + point = p, + radius = theRadius + } + } + + -- now call search + world.searchObjects(aCat, args, dcsCommon.objectHandler, collector) + return collector +end + function dcsCommon.getSceneryObjectsInZone(theZone) -- DCS ZONE!!! local aCat = 5 -- scenery -- WARNING: WE ARE USING DCS ZONES, NOT CFX!!! diff --git a/modules/persistence.lua b/modules/persistence.lua index ebd29e1..a2f8484 100644 --- a/modules/persistence.lua +++ b/modules/persistence.lua @@ -1,5 +1,5 @@ persistence = {} -persistence.version = "1.0.3" +persistence.version = "1.0.4" persistence.ups = 1 -- once every 1 seconds persistence.verbose = false persistence.active = false @@ -24,10 +24,11 @@ persistence.requiredLibs = { 1.0.2 - QoL when verbosity is on 1.0.3 - no longer always tells " mission saved to" new 'saveNotification" can be off + 1.0.4 - new optional 'root' property PROVIDES LOAD/SAVE ABILITY TO MODULES - PROVIDES STANDALONE/HOSTED SERVER COMPATIOBILITY + PROVIDES STANDALONE/HOSTED SERVER COMPATIBILITY --]]-- @@ -255,7 +256,6 @@ function persistence.initFlagsFromData(theFlags) trigger.action.outText("+++persistence: no flags loaded, commencing mission data load", 30) end - end function persistence.missionStartDataLoad() @@ -427,7 +427,24 @@ function persistence.readConfigZone() end -- serverDir is the path from the server save directory, usually "Missions/". - -- will be added to lfs.writedir(). + -- will be added to lfs.writedir() unless given a root attribute + if cfxZones.hasProperty(theZone, "root") then + -- we split this to enable further processing down the + -- line if neccessary + persistence.root = cfxZones.getStringFromZoneProperty(theZone, "root", lfs.writedir()) -- safe default + if not dcsCommon.stringEndsWith(persistence.root, "\\") then + persistence.root = persistence.root .. "\\" + end + if theZone.verbose then + trigger.action.outText("+++persistence: setting root to <" .. persistence.root .. ">", 30) + end + else + persistence.root = lfs.writedir() -- safe defaulting + if theZone.verbose then + trigger.action.outText("+++persistence: defaulting root to <" .. persistence.root .. ">", 30) + end + end + persistence.serverDir = cfxZones.getStringFromZoneProperty(theZone, "serverDir", "Missions\\") if hasConfig then @@ -509,13 +526,13 @@ function persistence.start() end end - local mainDir = lfs.writedir() .. persistence.serverDir +-- local mainDir = lfs.writedir() .. persistence.serverDir + local mainDir = persistence.root .. persistence.serverDir if not dcsCommon.stringEndsWith(mainDir, "\\") then mainDir = mainDir .. "\\" end -- lets see if we can access the server's mission directory and -- save directory - -- we first try to access server's main mission directory, called "mainDir" which is usually /Missions/> if persistence.isDir(mainDir) then if persistence.verbose then @@ -563,6 +580,7 @@ function persistence.start() end end + -- missionDir is root + serverDir + saveDir persistence.missionDir = missionDir persistence.active = true -- we can load and save data diff --git a/modules/williePete.lua b/modules/williePete.lua index aa79fc2..1802356 100644 --- a/modules/williePete.lua +++ b/modules/williePete.lua @@ -1,7 +1,8 @@ williePete = {} williePete.version = "1.0.0" williePete.ups = 10 -- we update at 10 fps, so accuracy of a --- mach two missile is within 33 meters, with interpolation even less +-- missile moving at Mach 2 is within 33 meters, +-- with interpolation even at 3 meters williePete.requiredLibs = { "dcsCommon", -- always @@ -12,8 +13,11 @@ williePete.requiredLibs = { williePete.willies = {} williePete.wpZones = {} williePete.playerGUIs = {} -- used for unit guis +williePete.blastedObjects = {} -- used when we detonate something -williePete.smokeWeapons = {"HYDRA_70_M274","HYDRA_70_MK61","HYDRA_70_MK1","HYDRA_70_WTU1B","BDU_45B","BDU_33","BDU_45","BDU_45LGB","BDU_50HD","BDU_50LD","BDU_50LGB","C_8CM"} +-- recognizes WP munitions. May require regular update when new +-- models come out. +williePete.smokeWeapons = {"HYDRA_70_M274","HYDRA_70_MK61","HYDRA_70_MK1","HYDRA_70_WTU1B","HYDRA_70_M156","HYDRA_70_M158","BDU_45B","BDU_33","BDU_45","BDU_45LGB","BDU_50HD","BDU_50LD","BDU_50LGB","C_8CM"} function williePete.addWillie(theWillie) table.insert(williePete.willies, theWillie) @@ -72,8 +76,6 @@ function williePete.getClosestZoneForCoa(point, coa) currDelta = delta closestZone = zData end - else --- trigger.outText("Zone <" .. zData.name .. ">, coa <" .. zData.coalition .. "> does not match <" .. coa .. ">", 30) end end return closestZone, currDelta @@ -91,19 +93,20 @@ function williePete.createWPZone(aZone) aZone.readyTime = 0 -- if readyTime > now we are not ready aZone.trackingPlayer = nil -- name player's unit who is being tracked for wp. may not be neccessary aZone.checkedIn = {} -- dict of all planes currently checked in - aZone.trackingEndsTime = 0 -- if now > trackingends, we remove player and send a message - aZone.wpTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "wpTriggerMethod", "change") - - aZone.FACTypes = cfxZones.getStringFromZoneProperty(aZone, "facTypes", "all") + aZone.wpMethod = cfxZones.getStringFromZoneProperty(aZone, "wpMethod", "change") aZone.checkInRange = cfxZones.getNumberFromZoneProperty(aZone, "checkInRange", williePete.checkInRange) -- default to my default aZone.ackSound = cfxZones.getStringFromZoneProperty(aZone, "ackSound", williePete.ackSound) aZone.guiSound = cfxZones.getStringFromZoneProperty(aZone, "guiSound", williePete.guiSound) - if cfxZones.hasProperty(aZone, "triggerMethod") then - aZone.wpTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "triggerMethod", "change") + if cfxZones.hasProperty(aZone, "method") then + aZone.wpMethod = cfxZones.getStringFromZoneProperty(aZone, "method", "change") + end + + if cfxZones.hasProperty(aZone, "wpFire!") then + aZone.wpFire = cfxZones.getStringFromZoneProperty(aZone, "wpFire!", " is in blast Radius (" .. blastRad .. "m) of shells for <" .. uName .. ">'s target coords", 30) + end + williePete.blastedObjects[aName] = uName -- last one gets the kill + end + end trigger.action.explosion(args.point, args.strength) - data = {} - data.point = args.point - data.strength = args.strength --- cfxArtilleryZones.invokeCallbacksFor('impact', args.zone, data) + end -function williePete.doParametricFireAt(aPoint, accuracy, shellNum, shellBaseStrength, shellVariance, transitionTime) +function williePete.doParametricFireAt(aPoint, accuracy, shellNum, shellBaseStrength, shellVariance, transitionTime, unitInfo) if williePete.verbose then trigger.action.outText("fire with accuracy <" .. accuracy .. "> shellNum <" .. shellNum .. "> baseStren <" .. shellBaseStrength .. "> variance <" .. shellVariance .. ">, ttime <" .. transitionTime .. ">", 30) end @@ -200,6 +214,7 @@ function williePete.doParametricFireAt(aPoint, accuracy, shellNum, shellBaseStre thePoint.y = land.getHeight({x = thePoint.x, y = thePoint.z}) + 1 -- elevate to ground height + 1 boomArgs.point = thePoint boomArgs.zone = aZone + boomArgs.unitInfo = unitInfo local timeVar = 5 * (2 * dcsCommon.randomPercent() - 1.0) -- +/- 1.5 seconds if timeVar < 0 then timeVar = -timeVar end @@ -218,6 +233,12 @@ end function williePete.doCheckIn(unitInfo) --trigger.action.outText("check-in received", 30) local theUnit = Unit.getByName(unitInfo.name) + if not theUnit then + -- dead man calling. Pilot dead but unit still alive + trigger.action.outText("Calling station, say again, can't read you.", 30) + return + end + local p = theUnit:getPoint() local theZone, dist = williePete.closestCheckInTgtZoneForCoa(p, unitInfo.coa) @@ -245,12 +266,12 @@ function williePete.doCheckIn(unitInfo) theZone.checkedIn[unitInfo.name] = unitInfo -- add the 'Target marked' menu - unitInfo.targetMarked = missionCommands.addCommandForGroup(unitInfo.gID, "Target Marked", unitInfo.root, williePete.redirectTargetMarked, unitInfo) + unitInfo.targetMarked = missionCommands.addCommandForGroup(unitInfo.gID, "Target Marked, commence firing", unitInfo.root, williePete.redirectTargetMarked, unitInfo) -- remove 'check in' missionCommands.removeItemForGroup(unitInfo.gID, unitInfo.checkIn) unitInfo.checkIn = nil -- add 'check out' - unitInfo.checkOut = missionCommands.addCommandForGroup(unitInfo.gID, "Check Out", unitInfo.root, williePete.redirectCheckOut, unitInfo) + unitInfo.checkOut = missionCommands.addCommandForGroup(unitInfo.gID, "Check Out of " .. theZone.name, unitInfo.root, williePete.redirectCheckOut, unitInfo) trigger.action.outTextForGroup(unitInfo.gID, "Roger " .. unitInfo.name .. ", " .. theZone.name .. " tracks you, standing by for target data.", 30) trigger.action.outSoundForGroup(unitInfo.gID, theZone.guiSound) @@ -261,8 +282,6 @@ function williePete.redirectCheckOut(unitInfo) end function williePete.doCheckOut(unitInfo) - --trigger.action.outText("check-out received", 30) - -- check out of all zones local wasCheckedIn = false local fromZone = "" @@ -305,10 +324,15 @@ function williePete.rogerDodger(args) end function williePete.doTargetMarked(unitInfo) - --trigger.action.outText("mark received", 30) -- first, check if we are past the time-out local now = timer.getTime() + if not unitInfo.wpInZone then + trigger.action.outTextForGroup(unitInfo.gID, "No target mark visible, please mark again", 30) + trigger.action.outSoundForGroup(unitInfo.gID, williePete.guiSound) + return + end + -- now check if zone matches check-in if not unitInfo.expiryTime or unitInfo.expiryTime < now then trigger.action.outTextForGroup(unitInfo.gID, "Target mark stale or ambiguous, set fresh mark", 30) @@ -357,13 +381,15 @@ function williePete.doTargetMarked(unitInfo) local transitionTime = tgtZone.transitionTime local accuracy = tgtZone.baseAccuracy - williePete.doParametricFireAt(unitInfo.pos, accuracy, shellNum, shellStrength, 0.2, transitionTime) + williePete.doParametricFireAt(unitInfo.pos, accuracy, shellNum, shellStrength, 0.2, transitionTime, unitInfo) -- set zone's cooldown tgtZone.readyTime = now + tgtZone.coolDown - -- erase player's wp mark - unitInfo.wpInZone = nil - unitInfo.pos = nil + + -- if we have an output, trigger it now + if tgtZone.wpFire then + cfxZones.pollFlag(tgtZone.wpFire, tgtZone.wpMethod, tgtZone) + end end -- return true if a zone is actively tracking theUnit to place -- a wp @@ -381,16 +407,43 @@ function williePete.isWP(theWeapon) for idx, wpw in pairs(williePete.smokeWeapons) do if theDesc == wpw then return true end end + trigger.action.outText(theDesc .. " is no wp, ignoring.", 30) return false end +function williePete.zedsDead(theObject) + if not theObject then return end + + local theName = theObject:getName() + -- now check if it's a registered blasted object:getSampleRate() + local blaster = williePete.blastedObjects[theName] + if blaster then + local theUnit = Unit.getByName(blaster) + if theUnit then + -- interface to playerscore + if cfxPlayerScore then + local fakeEvent = {} + fakeEvent.initiator = theUnit -- killer + fakeEvent.target = theObject -- vic + cfxPlayerScore.killDetected(fakeEvent) + end + end + williePete.blastedObjects[theName] = nil + end +end + function williePete:onEvent(event) if not event.initiator then - --trigger.action.outText("onEvent - " .. event.id .. ": no initiator",30) return end + + -- check if it's a dead event + if event.id == 8 then + -- death event + williePete.zedsDead(event.initiator) + end + if not event.weapon then - --trigger.action.outText("onEvent - " .. event.id .. ": no WEAPON",30) return end @@ -401,21 +454,18 @@ function williePete:onEvent(event) if event.id == 1 then -- S_EVENT_SHOT -- initiator is who fired. maybe want to test if player - --trigger.action.outText(theUnit:getName() .. " " .. pType .. " fired " .. event.weapon:getTypeName() .. ".", 30) - if not williePete.isWP(event.weapon) then - --trigger.action.outText("<" .. event.weapon:getTypeName() .. "> not a smoke weapon", 30) + -- we only trigger on WP weapons return end -- make sure that whoever fired it is being tracked by -- a zone if not williePete.zoneIsTracking(theUnit) then - --trigger.action.outText("<" .. event.weapon:getTypeName() .. "> fired while not being tracked by zone", 30) return end - -- assuming it's a willie, let's track it + -- it's a willie, fired by player who is checked in: let's track it local theWillie = {} theWillie.firedBy = theUnit:getName() theWillie.theUnit = theUnit @@ -427,16 +477,18 @@ function williePete:onEvent(event) williePete.addWillie(theWillie) end +--[[-- if event.id == 2 then -- hit local what = "something" if event.target then what = event.target:getName() end --trigger.action.outText("Weapon " .. event.weapon:getTypeName() .. " fired by unit ".. theUnit:getName() .. " " .. pType .. " hit " .. what, 30) -- may need to remove willie from willies end +--]]-- end --- test if a projectile hit ground inside a wp zone +-- test if a projectile has hit the ground inside a wp zone function williePete.isInside(theWillie) local thePoint = theWillie.pos local theUnitName = theWillie.firedBy -- may be dead already, but who cares @@ -457,8 +509,6 @@ function williePete.isInside(theWillie) end -- if we want to allow neutral zones (doens't make sense) -- add another guard below - else - --trigger.action.outText("willie outside " .. theZone.name, 30) end end return nil @@ -471,7 +521,6 @@ function williePete.projectileHit(theWillie) -- interpolate pos: half time between updates times last velocity local vmod = dcsCommon.vMultScalar(theWillie.v, 0.5 / williePete.ups) theWillie.pos = dcsCommon.vAdd(theWillie.pos, vmod) - --trigger.action.outText("Willie " .. theWillie.wt .. " expired at " .. dcsCommon.point2text(theWillie.pos) .. " interpolated.", 30) -- reset last mark for player local thePlayer = williePete.playerGUIs[theWillie.firedBy] @@ -493,15 +542,6 @@ function williePete.projectileHit(theWillie) thePlayer.pos = theWillie.pos -- remember the loc thePlayer.wpInZone = theZone -- remember the zone - -- mark point with smoke blue - --dcsCommon.markPointWithSmoke(theWillie.pos, 4) - - if cfxArtilleryZones then - --cfxArtilleryZones.doParametricFireAt(theWillie.pos, 50, 10) - else - -- mark point with smoke blue - --dcsCommon.markPointWithSmoke(theWillie.pos, 4) - end end function williePete.updateWP() @@ -635,14 +675,3 @@ if not williePete.start() then trigger.action.outText("cf/x Willie Pete aborted: missing libraries", 30) williePete = nil end - ---[[-- - Mechanics: - - unit checks in with arty zone. if not in range of arty zone + safe dist, error 'not in range' is returned, else is sent. . Zone will advise on status change when checked in. - - if unit leaves arty zone + safe dist, is displayed and is invoked - - unit can check out any time - - when checked in, any wp hitting the ground is remembered if still inside target zone - - player then gives 'target marked' - - if artillery on cooldown, or wp not inside zone error, else fire sequence starts, and cooldown starts for entire zone - ---]]-- \ No newline at end of file diff --git a/modules/xFlags.lua b/modules/xFlags.lua index 6342d38..c582b59 100644 --- a/modules/xFlags.lua +++ b/modules/xFlags.lua @@ -1,5 +1,5 @@ xFlags = {} -xFlags.version = "1.3.0" +xFlags.version = "1.3.1" xFlags.verbose = false xFlags.hiVerbose = false xFlags.ups = 1 -- overwritten in get config when configZone is present @@ -27,7 +27,13 @@ xFlags.requiredLibs = { - hiVerbose option - corrected bug in reset checksum 1.3.0 - xCount! flag - - "never" operator + - "never" operator + 1.3.1 - guards for xtriggerOffFlag and xtriggerOnFlag + to prevent QoL warnings + - guards for xDirect + - guards for xCount + + --]]-- xFlags.xFlagZones = {} @@ -96,9 +102,13 @@ function xFlags.createXFlagsWithZone(theZone) theZone.xChange = cfxZones.getStringFromZoneProperty(theZone, "xChange!", "*") end - theZone.xDirect = cfxZones.getStringFromZoneProperty(theZone, "xDirect", "*") + if cfxZones.hasProperty(theZone, "xDirect") then + theZone.xDirect = cfxZones.getStringFromZoneProperty(theZone, "xDirect", "*") + end - theZone.xCount = cfxZones.getStringFromZoneProperty(theZone, "xCount", "*") + if cfxZones.hasProperty(theZone, "xCount") then + theZone.xCount = cfxZones.getStringFromZoneProperty(theZone, "xCount", "*") + end theZone.inspect = cfxZones.getStringFromZoneProperty(theZone, "require", "or") -- same as any -- supported any/or, all/and, moreThan, atLeast, exactly @@ -132,11 +142,14 @@ function xFlags.createXFlagsWithZone(theZone) trigger.action.outText("+++xFlg: <" .. theZone.name .. "> starts suspended", 30) end - theZone.xtriggerOnFlag = cfxZones.getStringFromZoneProperty(theZone, "xOn?", "*") - theZone.xlastTriggerOnValue = cfxZones.getFlagValue(theZone.xtriggerOnFlag, theZone) - - theZone.xtriggerOffFlag = cfxZones.getStringFromZoneProperty(theZone, "xOff?", "*") - theZone.xlastTriggerOffValue = cfxZones.getFlagValue(theZone.xtriggerOffFlag, theZone) + if cfxZones.hasProperty(theZone, "xOn?") then + theZone.xtriggerOnFlag = cfxZones.getStringFromZoneProperty(theZone, "xOn?", "*") + theZone.xlastTriggerOnValue = cfxZones.getFlagValue(theZone.xtriggerOnFlag, theZone) + end + if cfxZones.hasProperty(theZone, "xOff?") then + theZone.xtriggerOffFlag = cfxZones.getStringFromZoneProperty(theZone, "xOff?", "*") + theZone.xlastTriggerOffValue = cfxZones.getFlagValue(theZone.xtriggerOffFlag, theZone) + end end function xFlags.evaluateNumOrFlag(theAttribute, theZone) @@ -363,15 +376,18 @@ function xFlags.evaluateZone(theZone) -- now directly set the value of evalResult (0 = false, 1 = true) -- to "xDirect". Always sets output to current result of evaluation -- true (1)/false(0), no matter if changed or not - - if evalResult then - cfxZones.setFlagValueMult(theZone.xDirect, 1, theZone) - else - cfxZones.setFlagValueMult(theZone.xDirect, 0, theZone) - end + if theZone.xDirect then + if evalResult then + cfxZones.setFlagValueMult(theZone.xDirect, 1, theZone) + else + cfxZones.setFlagValueMult(theZone.xDirect, 0, theZone) + end + end -- directly set the xCount flag - cfxZones.setFlagValueMult(theZone.xCount, hits, theZone) + if theZone.xCount then + cfxZones.setFlagValueMult(theZone.xCount, hits, theZone) + end -- now see if we bang the output according to method if evalResult then @@ -391,14 +407,14 @@ function xFlags.update() for idx, theZone in pairs (xFlags.xFlagZones) do -- see if we should suspend - if cfxZones.testZoneFlag(theZone, theZone.xtriggerOnFlag, "change", "xlastTriggerOnValue") then + if theZone.xtriggerOnFlag and cfxZones.testZoneFlag(theZone, theZone.xtriggerOnFlag, "change", "xlastTriggerOnValue") then if xFlags.verbose or theZone.verbose then trigger.action.outText("+++xFlg: enabling " .. theZone.name, 30) end theZone.xSuspended = false end - if cfxZones.testZoneFlag(theZone, theZone.xtriggerOffFlag, "change", "xlastTriggerOffValue") then + if theZone.xtriggerOffFlag and cfxZones.testZoneFlag(theZone, theZone.xtriggerOffFlag, "change", "xlastTriggerOffValue") then if xFlags.verbose or theZone.verbose then trigger.action.outText("+++xFlg: DISabling " .. theZone.name, 30) end diff --git a/tutorial & demo missions/distressbeacon.ogg b/sound FX/distressbeacon.ogg similarity index 100% rename from tutorial & demo missions/distressbeacon.ogg rename to sound FX/distressbeacon.ogg diff --git a/sound FX/roger that click half.ogg b/sound FX/roger that click half.ogg new file mode 100644 index 0000000..b5a4415 Binary files /dev/null and b/sound FX/roger that click half.ogg differ diff --git a/tutorial & demo missions/demo - Slot-Blocking and you.miz b/tutorial & demo missions/demo - Slot-Blocking and you.miz new file mode 100644 index 0000000..54dcd41 Binary files /dev/null and b/tutorial & demo missions/demo - Slot-Blocking and you.miz differ diff --git a/tutorial & demo missions/demo - feats and autoCSAR.miz b/tutorial & demo missions/demo - feats and autoCSAR.miz new file mode 100644 index 0000000..b839730 Binary files /dev/null and b/tutorial & demo missions/demo - feats and autoCSAR.miz differ