diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 4a9a61b..389b4a7 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 f5734cf..08bcf78 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/cfxNDB.lua b/modules/NDB.lua similarity index 100% rename from modules/cfxNDB.lua rename to modules/NDB.lua diff --git a/modules/cfxSSBClient.lua b/modules/SSBClient.lua similarity index 100% rename from modules/cfxSSBClient.lua rename to modules/SSBClient.lua diff --git a/modules/cfxSSBSingleUse.lua b/modules/SSBSingleUse.lua similarity index 70% rename from modules/cfxSSBSingleUse.lua rename to modules/SSBSingleUse.lua index f8a3fc1..aae2ffd 100644 --- a/modules/cfxSSBSingleUse.lua +++ b/modules/SSBSingleUse.lua @@ -1,25 +1,22 @@ cfxSSBSingleUse = {} -cfxSSBSingleUse.version = "1.1.0" - ---[[-- -Version History - 1.0.0 - Initial version - 1.1.0 - importing dcsCommon, cfxGroup for simplicity - - save unit name on player enter unit as look-up - - determining ground-start - - place wreck in slot - 1.1.1 - guarding against nil playerName - - using 15 (birth) instead of 20 (player enter) -WHAT IT IS -SSB Single Use is a script that blocks a player slot -after that plane crashes. - - ---]]-- - cfxSSBSingleUse.enabledFlagValue = 0 -- DO NOT CHANGE, MUST MATCH SSB cfxSSBSingleUse.disabledFlagValue = cfxSSBSingleUse.enabledFlagValue + 100 -- DO NOT CHANGE +cfxSSBSingleUse.version = "2.0.0" +cfxSSBSingleUse.verbose = true +cfxSSBSingleUse.useDebris = false +cfxSSBSingleUse.requiredLibs = { + "dcsCommon", -- no cfxZones. a rarity! + "cfxMX", +} +--[[-- +Version History + 2.0.0 - cfxMX homologization + - startup checks + - minimal verbosity + - useDebris switch, currently set off + - clean-up +--]]-- cfxSSBSingleUse.playerUnits = {} cfxSSBSingleUse.slotGroundActions = { @@ -29,7 +26,6 @@ cfxSSBSingleUse.slotGroundActions = { "From Ground Area", "From Ground Area Hot", } - cfxSSBSingleUse.groundSlots = {} -- players that start on the ground function cfxSSBSingleUse:onEvent(event) @@ -40,7 +36,6 @@ function cfxSSBSingleUse:onEvent(event) -- if we get here, initiator is set local theUnit = event.initiator -- we know this exists - -- write down player names and planes if event.id == 15 then local uName = theUnit:getName() @@ -69,7 +64,9 @@ function cfxSSBSingleUse:onEvent(event) if not thePilot then -- ignore. not a player plane - trigger.action.outText("+++singleUse: ignored crash for NPC unit <" .. uName .. ">", 30) + if cfxSSBSingleUse.verbose then + trigger.action.outText("+++singleUse: ignored crash for NPC unit <" .. uName .. ">", 30) + end return end @@ -80,19 +77,24 @@ function cfxSSBSingleUse:onEvent(event) local theGroundSlot = cfxSSBSingleUse.groundSlots[gName] if theGroundSlot then local unitType = theUnit:getTypeName() - trigger.action.outText("+++singleUse: <" .. uName .. "> starts on Ground. Will place debris for " .. unitType .. " NOW!!!", 30) + if cfxSSBSingleUse.verbose then + trigger.action.outText("+++singleUse: <" .. uName .. "> starts on Ground. Will place debris for " .. unitType .. " NOW!!!", 30) + end cfxSSBSingleUse.placeDebris(unitType, theGroundSlot) end -- block this slot. trigger.action.setUserFlag(gName, cfxSSBSingleUse.disabledFlagValue) - - trigger.action.outText("+++singleUse: blocked <" .. gName .. "> after " .. thePilot .. " crashed it.", 30) + if cfxSSBSingleUse.verbose then + trigger.action.outText("+++singleUse: blocked <" .. gName .. "> after " .. thePilot .. " crashed it.", 30) + end end end function cfxSSBSingleUse.placeDebris(unitType, theGroundSlot) if not unitType then return end + if not cfxSSBSingleUse.useDebris then return end + -- access location one, we assume single-unit groups -- or at least that the player sits in unit one local playerData = theGroundSlot.playerUnits @@ -106,12 +108,14 @@ function cfxSSBSingleUse.placeDebris(unitType, theGroundSlot) wreckData.type = unitType coalition.addStaticObject(theGroundSlot.coaNum, wreckData ) - trigger.action.outText("+++singleUse: wreck <" .. unitType .. "> at " .. wreckData.x .. ", " .. wreckData.y .. " for " .. wreckData.name, 30) + if cfxSSBSingleUse.verbose then + trigger.action.outText("+++singleUse: wreck <" .. unitType .. "> at " .. wreckData.x .. ", " .. wreckData.y .. " for " .. wreckData.name, 30) + end end function cfxSSBSingleUse.populateAirfieldSlots() - local pGroups = cfxGroups.getPlayerGroup() + local pGroups = cfxMX.getPlayerGroup() local groundStarters = {} for idx, theGroup in pairs(pGroups) do -- we always use the first player's plane as referenced @@ -122,33 +126,35 @@ function cfxSSBSingleUse.populateAirfieldSlots() if dcsCommon.arrayContainsString(cfxSSBSingleUse.slotGroundActions, action ) then -- ground starter, not from runway groundStarters[theGroup.name] = theGroup - trigger.action.outText("+++singleUse: <" .. theGroup.name .. "> is ground starter", 30) + if cfxSSBSingleUse.verbose then + trigger.action.outText("+++singleUse: <" .. theGroup.name .. "> is ground starter", 30) + end end end cfxSSBSingleUse.groundSlots = groundStarters end function cfxSSBSingleUse.start() + -- check libs + if not dcsCommon.libCheck("cfx SSB Single Use", + cfxSSBSingleUse.requiredLibs) then + return false + end + -- install event monitor world.addEventHandler(cfxSSBSingleUse) - -- get all groups and process them to find - -- all planes that are on the ground for - -- eye candy cfxSSBSingleUse.populateAirfieldSlots() -- turn on ssb trigger.action.setUserFlag("SSB",100) trigger.action.outText("SSB Single use v" .. cfxSSBSingleUse.version .. " running", 30) + return true end -- let's go! -cfxSSBSingleUse.start() - ---[[-- -Additional features (later): -- place a wreck in slot when blocking for eye candy -- record player when they enter a unit and only block player planes - ---]]-- \ No newline at end of file +if not cfxSSBSingleUse.start()then + trigger.action.outText("SSB Single Use failed to start up.", 30) + cfxSSBSingleUse = nil +end diff --git a/modules/TDZ.lua b/modules/TDZ.lua index 4549abb..380d60b 100644 --- a/modules/TDZ.lua +++ b/modules/TDZ.lua @@ -1,5 +1,5 @@ tdz = {} -tdz.version = "1.0.1" +tdz.version = "1.0.2" tdz.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -15,6 +15,8 @@ VERSION HISTORY multiple zone support hops detection improvement helo attribute + 1.0.2 - manual placement option + filters FARPs --]]-- @@ -80,24 +82,41 @@ end -- function tdz.createTDZ(theZone) local p = theZone:getPoint() - local theBase = dcsCommon.getClosestAirbaseTo(p) -- never get FARPS + local theBase = dcsCommon.getClosestAirbaseTo(p, 0) -- never get FARPS theZone.base = theBase theZone.baseName = theBase:getName() theZone.helos = false - -- get closest runway to TDZ - -- may get a bit hairy, so let's find a good way - local allRwys = theBase:getRunways() local nearestRwy = nil - local minDist = math.huge - for idx, aRwy in pairs(allRwys) do - local rp = aRwy.position - local dist = dcsCommon.distFlat(p, rp) - if dist < minDist then - nearestRwy = aRwy - minDist = dist + -- see if this is a manually placed runway + if theZone:getBoolFromZoneProperty("manual", true) then + -- construct runway from trigger zone attributes + if theZone.verbose or tdz.verbose then + trigger.action.outText("+++TDZ: runway for <" .. theZone.name .. "> is manually placed", 30) + end + nearestRwy = {} + nearestRwy.length = theZone:getNumberFromZoneProperty("length", 2500) + nearestRwy.width = theZone:getNumberFromZoneProperty("width", 60) + local hdgRaw = theZone:getNumberFromZoneProperty("course", 0) -- in degrees + hdgRaw = hdgRaw * 0.0174533 -- rads + nearestRwy.course = hdgRaw * (-1) -- why? because DCS. + nearestRwy.position = theZone:getPoint(true) + theZone.baseName = theZone.name .. "(manual placement)" + else + -- get closest runway to TDZ + -- may get a bit hairy, so let's find a good way + local allRwys = theBase:getRunways() + + local minDist = math.huge + for idx, aRwy in pairs(allRwys) do + local rp = aRwy.position + local dist = dcsCommon.distFlat(p, rp) + if dist < minDist then + nearestRwy = aRwy + minDist = dist + end end - end + end local bearing = nearestRwy.course * (-1) if bearing < 0 then bearing = bearing + math.pi * 2 end theZone.bearing = bearing diff --git a/modules/airfield.lua b/modules/airfield.lua index a6f2977..e3f10e0 100644 --- a/modules/airfield.lua +++ b/modules/airfield.lua @@ -1,13 +1,13 @@ airfield = {} -airfield.version = "1.1.2" +airfield.version = "2.0.0" airfield.requiredLibs = { "dcsCommon", "cfxZones", } -airfield.verbose = false airfield.myAirfields = {} -- indexed by name -airfield.farps = false airfield.gracePeriod = 3 +airfield.allAirfields = {} -- inexed by name + --[[-- This module generates signals when the nearest airfield changes hands, can force the coalition of an airfield, and always provides the @@ -24,17 +24,47 @@ airfield.gracePeriod = 3 1.1.2 - 'show' attribute line color attributes per zone line color defaults in config + 2.0.0 - show all airfields option + - fully reworked show options + - unmanaged airfields are automatically updated + - full color support + -- support for FARPS as well + --]]-- +-- init all airfields DB +function airfield.collectAll() + local allBases = world.getAirbases() -- get all + local count = 0 + local dropped = 0 + for idx, aBase in pairs(allBases) do + local entry = {} + local cat = Airbase.getCategory(aBase) -- DCS 2.9 hardened + -- cats: 0 = airfield, 1 = farp, 2 = ship + if (cat == 0) or (cat == 1) then + local name = aBase:getName() + entry.base = aBase + entry.cat = cat + -- entry.linkedTo holds zone if linked to. that how we know + airfield.allAirfields[name] = entry + count = count + 1 + else + dropped = dropped + 1 + end + end + if airfield.verbose then + trigger.action.outText("+++airF: init - count = <" .. count .. ">, dropped = <" .. dropped .. ">", 30) + end +end + -- -- setting up airfield -- function airfield.createAirFieldFromZone(theZone) ---trigger.action.outText("Enter airfield for <" .. theZone.name .. ">", 30) theZone.farps = theZone:getBoolFromZoneProperty("farps", false) local filterCat = 0 - if (airfield.farps or theZone.farps) then filterCat = {0, 1} end -- bases and farps + if (theZone.farps) then filterCat = {0, 1} end -- bases and farps local p = theZone:getPoint() local theBase = dcsCommon.getClosestAirbaseTo(p, filterCat) theZone.airfield = theBase @@ -113,9 +143,29 @@ function airfield.createAirFieldFromZone(theZone) theZone.neutralFill = theZone:getRGBAVectorFromZoneProperty("neutralFill", airfield.neutralFill) airfield.showAirfield(theZone) + + -- now mark this zone as handled + local entry = airfield.allAirfields[theZone.afName] + entry.linkedTo = theZone -- only remember last, but that's enough. ---trigger.action.outText("Exit airfield for <" .. theZone.name .. ">", 30) - +end + +function airfield.markAirfieldOnMap(theAirfield, lineColor, fillColor) + local markID = dcsCommon.numberUUID() + local radius = 2000 -- meters + local p = theAirfield:getPoint() + -- if there are runways, we center on first runway + local rws = theAirfield:getRunways() + if rws then -- all airfields and farps have runways defined, but that array isnt filled for FARPS + local rw1 = rws[1] + if rw1 then + p.x = rw1.position.x + p.z = rw1.position.z + end + end + p.y = 0 + trigger.action.circleToAll(-1, markID, p, radius, lineColor, fillColor, 1, true, "") + return markID end function airfield.showAirfield(theZone) @@ -138,35 +188,9 @@ function airfield.showAirfield(theZone) fillColor = theZone.neutralFill -- {0.8, 0.8, 0.8, 0.2} end - -- always center on airfield, always 2km radius - local markID = dcsCommon.numberUUID() - local radius = 2000 -- meters - local p = theZone.airfield:getPoint() - -- if there are runways, we center on first runway - local rws = theZone.airfield:getRunways() - if rws then -- all airfields and farps have runways, but that array isnt filled for FARPS - local rw1 = rws[1] - if rw1 then - p.x = rw1.position.x - p.z = rw1.position.z - if airfield.verbose or theZone.verbose then - trigger.action.outText("+++airF: zone <" .. theZone.name .. "> assoc airfield <" .. theZone.afName .. "> has rw1, x=" .. p.x .. ", z=" .. p.z, 30) - end - else - if airfield.verbose or theZone.verbose then - trigger.action.outText("+++airF: zone <" .. theZone.name .. "> assoc airfield <" .. theZone.afName .. "> has no rw1", 30) - end - end - else - if airfield.verbose or theZone.verbose then - trigger.action.outText("+++airF: zone <" .. theZone.name .. "> assoc airfield <" .. theZone.afName .. "> has no runways", 30) - end - end - p.y = 0 - - trigger.action.circleToAll(-1, markID, p, radius, lineColor, fillColor, 1, true, "") - theZone.ownerMark = markID + theZone.ownerMark = airfield.markAirfieldOnMap(theZone.airfield, lineColor, fillColor) + end function airfield.assumeControl(theZone) @@ -188,12 +212,40 @@ end -- -- event handling -- +function airfield.untendedCapture(theName, theBase) + if airfield.showAll and airfield.allAirfields[theName] then + -- we draw and handle all airfields, even those + -- without an attached handler zone + local theEntry = airfield.allAirfields[theName] + if not theEntry.linkedTo then -- merely safety + if theEntry.ownerMark then + -- remove previous mark + trigger.action.removeMark(theEntry.ownerMark) + theEntry.ownerMark = nil + end + local owner = theBase:getCoalition() + local lineColor = airfield.redLine + local fillColor = airfield.redFill + if owner == 2 then + lineColor = airfield.blueLine + fillColor = airfield.blueFill + elseif owner == 0 or owner == 3 then + lineColor = airfield.neutralLine + fillColor = airfield.neutralFill + end + theEntry.ownerMark = airfield.markAirfieldOnMap(theBase, lineColor, fillColor) + end + end +end function airfield.airfieldCaptured(theBase) -- retrieve the zone that controls this airfield local bName = theBase:getName() local theZone = airfield.myAirfields[bName] - if not theZone then return end -- not handled + if not theZone then + airfield.untendedCapture(bName, theBase) + return + end -- not attached to a zone local newCoa = theBase:getCoalition() theZone.owner = newCoa @@ -227,15 +279,8 @@ function airfield:onEvent(event) -- get category local desc = theBase:getDesc() local bName = theBase:getName() - local cat = desc.category -- never get cat directly! ---[[-- if cat == 1 then - if not airfield.farps then - if airfield.verbose then - trigger.action.outText("+++airF: ignored cap event for FARP <" .. bName .. ">", 30) - end - return - end - end --]] + local cat = desc.category -- never get cat directly! DCS 2.0 safe + if airfield.verbose then trigger.action.outText("+++airF: cap event for <" .. bName .. ">, cat = (" .. cat .. ")", 30) end @@ -391,7 +436,7 @@ function airfield.readConfig() theZone = cfxZones.createSimpleZone("airfieldConfig") end airfield.verbose = theZone.verbose - airfield.farps = theZone:getBoolFromZoneProperty("farps", false) +-- airfield.farps = theZone:getBoolFromZoneProperty("farps", false) -- colors for line and fill airfield.redLine = theZone:getRGBAVectorFromZoneProperty("redLine", {1.0, 0, 0, 1.0}) @@ -400,11 +445,24 @@ function airfield.readConfig() airfield.blueFill = theZone:getRGBAVectorFromZoneProperty("blueFill", {0.0, 0, 1.0, 0.2}) airfield.neutralLine = theZone:getRGBAVectorFromZoneProperty("neutralLine", {0.8, 0.8, 0.8, 1.0}) airfield.neutralFill = theZone:getRGBAVectorFromZoneProperty("neutralFill", {0.8, 0.8, 0.8, 0.2}) + + airfield.showAll = theZone:getBoolFromZoneProperty("show", false) +end + +function airfield.showUnlinked() + for name, entry in pairs(airfield.allAirfields) do + if not entry.linkedTo then + airfield.untendedCapture(name, entry.base) + end + end end function airfield.start() if not dcsCommon.libCheck("cfx airfield", airfield.requiredLibs) then return false end + + -- set up DB + airfield.collectAll() -- read config airfield.readConfig() @@ -415,6 +473,9 @@ function airfield.start() airfield.createAirFieldFromZone(aZone) end + -- show all unlinked + if airfield.showAll then airfield.showUnlinked() end + -- connect event handler world.addEventHandler(airfield) @@ -442,7 +503,3 @@ if not airfield.start() then trigger.action.outText("+++ aborted airfield v" .. airfield.version .. " -- startup failed", 30) airfield = nil end - ---[[-- - ideas: what if we made this airfield movable? ---]]-- \ No newline at end of file diff --git a/modules/cfxArtilleryUI.lua b/modules/artilleryUI.lua similarity index 93% rename from modules/cfxArtilleryUI.lua rename to modules/artilleryUI.lua index fec06f1..c09ff7e 100644 --- a/modules/cfxArtilleryUI.lua +++ b/modules/artilleryUI.lua @@ -1,5 +1,5 @@ cfxArtilleryUI = {} -cfxArtilleryUI.version = "1.1.0" +cfxArtilleryUI.version = "2.0.0" cfxArtilleryUI.requiredLibs = { "dcsCommon", -- always "cfxPlayer", -- get all players @@ -8,7 +8,7 @@ cfxArtilleryUI.requiredLibs = { } -- -- UI for ArtilleryZones module, implements LOS, Observer, SMOKE --- Copyright (c) 2021, 2022 by Christian Franz and cf/x AG +-- Copyright (c) 2021 - 2024 by Christian Franz and cf/x AG --[[-- VERSION HISTORY - 1.0.0 - based on jtacGrpUI @@ -21,6 +21,8 @@ cfxArtilleryUI.requiredLibs = { - collect zones recognizes moving zones, updates landHeight - allSeeing god mode attribute: always observing. - allRanging god mode attribute: always in range. + - 2.0.0 - dmlZones, OOP + cleanup --]]-- @@ -272,21 +274,18 @@ function cfxArtilleryUI.populateTargetMenu(conf) -- now compare old control string with new, and only -- re-populate if the old is different if tgtCheckSum == conf.tgtCheckSum then --- trigger.action.outText("*** yeah old targets", 30) return elseif not conf.tgtCheckSum then --- trigger.action.outText("+++ new target menu", 30) + else trigger.action.outTextForGroup(conf.id, "Artillery target updates", 30) trigger.action.outSoundForGroup(conf.id, cfxArtilleryUI.updateSound) --- trigger.action.outText("!!! target update ", 30) + end -- we need to re-populate. erase old values cfxArtilleryUI.clearCommsTargets(conf) - conf.tgtCheckSum = tgtCheckSum -- remember for last time - --trigger.action.outText("new targets", 30) - + conf.tgtCheckSum = tgtCheckSum -- remember for last time if #filteredTargets < 1 then -- simply put one-line dummy in there local commandTxt = "(No unobscured target areas)" @@ -307,7 +306,6 @@ function cfxArtilleryUI.populateTargetMenu(conf) for i=1, numTargets do -- make a target command for each local aTarget = filteredTargets[i] - commandTxt = "Fire at: <" .. aTarget.name .. ">" theCommand = missionCommands.addCommandForGroup( conf.id, @@ -444,7 +442,6 @@ function cfxArtilleryUI.doCommandListTargets(args) local conf = args[1] -- < conf in here local what = args[2] -- < second argument in here local theGroup = conf.theGroup --- trigger.action.outTextForGroup(conf.id, "+++ groupUI: processing comms menu for <" .. what .. ">", 30) local targetList = cfxArtilleryUI.collectArtyTargets(conf) -- iterate the list if #targetList < 1 then @@ -453,7 +450,6 @@ function cfxArtilleryUI.doCommandListTargets(args) end local desc = "Artillery Targets:\n" --- trigger.action.outTextForGroup(conf.id, "Target Report:", 30) for i=1, #targetList do local aTarget = targetList[i] local inRange = cfxArtilleryUI.allRanging or aTarget.range * 1000 < aTarget.spotRange @@ -475,7 +471,6 @@ end function cfxArtilleryUI.collectArtyTargets(conf) -- iterate all target zones, for those that are on my side -- calculate range, bearing, and then order by distance - local theTargets = {} for idx, aZone in pairs(cfxArtilleryZones.artilleryZones) do if aZone.coalition == conf.coalition then @@ -498,7 +493,7 @@ function cfxArtilleryUI.collectArtyTargets(conf) --aTarget.targetName = aZone.name aTarget.spotRange = aZone.spotRange -- get the target we are lazing - local zP = cfxZones.getPoint(aZone) -- zone can move! + local zP = aZone:getPoint() -- zone can move! aZone.landHeight = land.getHeight({x = zP.x, y= zP.z}) local there = {x = zP.x, y = aZone.landHeight + 1, z=zP.z} aTarget.there = there @@ -654,18 +649,17 @@ function cfxArtilleryUI.readConfigZone() -- note: must match exactly!!!! local theZone = cfxZones.getZoneByName("ArtilleryUIConfig") if not theZone then - trigger.action.outText("+++A-UI: no config zone!", 30) - return + --trigger.action.outText("+++A-UI: no config zone!", 30) + --return + theZone = cfxZones.createSimpleZone("ArtilleryUIConfig") end - - trigger.action.outText("+++A-UI: found config zone!", 30) - - cfxArtilleryUI.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) - cfxArtilleryUI.allowPlanes = cfxZones.getBoolFromZoneProperty(theZone, "allowPlanes", false) - cfxArtilleryUI.smokeColor = cfxZones.getSmokeColorStringFromZoneProperty(theZone, "smokeColor", "red") - cfxArtilleryUI.allSeeing = cfxZones.getBoolFromZoneProperty(theZone, "allSeeing", false) - cfxArtilleryUI.allRanging = cfxZones.getBoolFromZoneProperty(theZone, "allRanging", false) - cfxArtilleryUI.allTiming = cfxZones.getBoolFromZoneProperty(theZone, "allTiming", false) + + cfxArtilleryUI.verbose = theZone.verbose + cfxArtilleryUI.allowPlanes = theZone:getBoolFromZoneProperty("allowPlanes", false) + cfxArtilleryUI.smokeColor = theZone:getSmokeColorStringFromZoneProperty("smokeColor", "red") + cfxArtilleryUI.allSeeing = theZone:getBoolFromZoneProperty("allSeeing", false) + cfxArtilleryUI.allRanging = theZone:getBoolFromZoneProperty("allRanging", false) + cfxArtilleryUI.allTiming = theZone:getBoolFromZoneProperty("allTiming", false) end -- @@ -681,7 +675,6 @@ function cfxArtilleryUI.start() cfxArtilleryUI.readConfigZone() -- iterate existing groups so we have a start situation - -- now iterate through all player groups and install the Assault Troop Menu local allPlayerGroups = cfxPlayerGroups -- cfxPlayerGroups is a global, don't fuck with it! -- contains per group player record. Does not resolve on unit level! for gname, pgroup in pairs(allPlayerGroups) do @@ -710,5 +703,5 @@ end --[[-- TODO: transition times based on distance - requires real bound arty first -DONE: ui for smoking target zone: list ten closest zones, and provide menu to smoke zone +TODO: remove dependency on cfxPlayer --]]-- \ No newline at end of file diff --git a/modules/cfxArtilleryZones.lua b/modules/artilleryZones.lua similarity index 82% rename from modules/cfxArtilleryZones.lua rename to modules/artilleryZones.lua index 4126f0b..4cf6816 100644 --- a/modules/cfxArtilleryZones.lua +++ b/modules/artilleryZones.lua @@ -1,5 +1,5 @@ cfxArtilleryZones = {} -cfxArtilleryZones.version = "2.2.2" +cfxArtilleryZones.version = "3.0.0" cfxArtilleryZones.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -31,28 +31,12 @@ cfxArtilleryZones.verbose = false 2.2.0 - DML Watchflag integration 2.2.1 - minor code clean-up 2.2.2 - new doParametricFireAt() - + 3.0.0 - dmlZones, OOP + - cleanup + Artillery Target Zones *** EXTENDS ZONES *** Target Zones for artillery. Can determine which zones are in range and visible and then handle artillery barrage to this zone - Copyright (c) 2021, 2022 by Christian Franz and cf/x AG - - USAGE - Via ME: Add the relevant attributes to the zone - Via Script: Use createArtilleryTarget() - - - Callbacks - when fire at target is invoked, a callback can be - invoked so your code knows that fire control has been - given a command or that projectiles are impacting. - Signature - callback(rason, zone, data), with - reason: 'firing' - fire command given for zone - 'impact' a projectile has hit - zone: artilleryZone - data: empty on 'fire' - .point where impact point - .strength power of explosion + Copyright (c) 2021 - 2024 by Christian Franz and cf/x AG --]]-- cfxArtilleryZones.artilleryZones = {} @@ -117,42 +101,39 @@ function cfxArtilleryZones.createArtilleryTarget(name, point, coalition, spotRan end function cfxArtilleryZones.processArtilleryZone(aZone) - aZone.artilleryTarget = cfxZones.getStringFromZoneProperty(aZone, "artilleryTarget", aZone.name) - aZone.coalition = cfxZones.getCoalitionFromZoneProperty(aZone, "coalition", 0) -- side that marks it on map, and who fires arty - aZone.spotRange = cfxZones.getNumberFromZoneProperty(aZone, "spotRange", 3000) -- FO max range to direct fire - aZone.shellStrength = cfxZones.getNumberFromZoneProperty(aZone, "shellStrength", 500) -- power of shells (strength) + aZone.artilleryTarget = aZone:getStringFromZoneProperty( "artilleryTarget", aZone.name) + aZone.coalition = aZone:getCoalitionFromZoneProperty("coalition", 0) -- side that marks it on map, and who fires arty + aZone.spotRange = aZone:getNumberFromZoneProperty("spotRange", 3000) -- FO max range to direct fire + aZone.shellStrength = aZone:getNumberFromZoneProperty("shellStrength", 500) -- power of shells (strength) - aZone.shellNum = cfxZones.getNumberFromZoneProperty(aZone, "shellNum", 17) -- number of shells in bombardment - aZone.transitionTime = cfxZones.getNumberFromZoneProperty(aZone, "transitionTime", 20) -- average time of travel for projectiles - aZone.addMark = cfxZones.getBoolFromZoneProperty(aZone, "addMark", true) -- note: defaults to true - aZone.shellVariance = cfxZones.getNumberFromZoneProperty(aZone, "shellVariance", 0.2) -- strength of explosion can vary by +/- this amount + aZone.shellNum = aZone:getNumberFromZoneProperty("shellNum", 17) -- number of shells in bombardment + aZone.transitionTime = aZone:getNumberFromZoneProperty( "transitionTime", 20) -- average time of travel for projectiles + aZone.addMark = aZone:getBoolFromZoneProperty("addMark", true) -- note: defaults to true + aZone.shellVariance = aZone:getNumberFromZoneProperty( "shellVariance", 0.2) -- strength of explosion can vary by +/- this amount -- watchflag: -- triggerMethod - aZone.artyTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "artyTriggerMethod", "change") + aZone.artyTriggerMethod = aZone:getStringFromZoneProperty( "artyTriggerMethod", "change") - if cfxZones.hasProperty(aZone, "triggerMethod") then - aZone.artyTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "triggerMethod", "change") + if aZone:hasProperty("triggerMethod") then + aZone.artyTriggerMethod = aZone:getStringFromZoneProperty("triggerMethod", "change") end - if cfxZones.hasProperty(aZone, "f?") then - aZone.artyTriggerFlag = cfxZones.getStringFromZoneProperty(aZone, "f?", "none") - end - - if cfxZones.hasProperty(aZone, "artillery?") then - aZone.artyTriggerFlag = cfxZones.getStringFromZoneProperty(aZone, "artillery?", "none") - end - if cfxZones.hasProperty(aZone, "in?") then - aZone.artyTriggerFlag = cfxZones.getStringFromZoneProperty(aZone, "in?", "none") + if aZone:hasProperty("f?") then + aZone.artyTriggerFlag = cfxZones.getStringFromZoneProperty("f?", "none") + elseif aZone:hasProperty("artillery?") then + aZone.artyTriggerFlag = aZone:getStringFromZoneProperty("artillery?", "none") + elseif aZone:hasProperty("in?") then + aZone.artyTriggerFlag = aZone:getStringFromZoneProperty("in?", "none") end if aZone.artyTriggerFlag then aZone.lastTriggerValue = trigger.misc.getUserFlag(aZone.artyTriggerFlag) -- save last value end - aZone.cooldown =cfxZones.getNumberFromZoneProperty(aZone, "cooldown", 120) -- seconds - aZone.baseAccuracy = cfxZones.getNumberFromZoneProperty(aZone, "baseAccuracy", aZone.radius) -- meters from center radius shell impact + aZone.cooldown = aZone:getNumberFromZoneProperty("cooldown", 120) -- seconds + aZone.baseAccuracy = aZone:getNumberFromZoneProperty( "baseAccuracy", aZone.radius) -- meters from center radius shell impact -- use zone radius as mase accuracy for simple placement - aZone.silent = cfxZones.getBoolFromZoneProperty(aZone, "silent", false) + aZone.silent = aZone:getBoolFromZoneProperty("silent", false) end function cfxArtilleryZones.addArtilleryZone(aZone) @@ -200,7 +181,7 @@ function cfxArtilleryZones.artilleryZonesInRangeOfUnit(theUnit) -- is it one of mine? if aZone.coalition == myCoalition then -- is it close enough? - local zP = cfxZones.getPoint(aZone) + local zP = aZone:getPoint() aZone.landHeight = land.getHeight({x = zP.x, y= zP.z}) local zonePoint = {x = zP.x, y = aZone.landHeight, z = zP.z} local d = dcsCommon.dist(p,zonePoint) @@ -249,7 +230,7 @@ end -- -- --- BOOM command +-- BOOM method -- function cfxArtilleryZones.doBoom(args) trigger.action.explosion(args.point, args.strength) @@ -356,7 +337,6 @@ function cfxArtilleryZones.simFireAtZone(aZone, aGroup, dist) trigger.action.outTextForCoalition(aGroup:getCoalition(), "Artillery firing on ".. aZone.name .. addInfo, 30) end - --trigger.action.smoke(center, 2) -- mark location visually end function cfxArtilleryZones.simSmokeZone(aZone, aGroup, aColor) @@ -367,7 +347,7 @@ function cfxArtilleryZones.simSmokeZone(aZone, aGroup, aColor) if type(aColor) == "string" then aColor = dcsCommon.smokeColor2Num(aColor) end - local zP = cfxZones.getPoint(aZone) + local zP = aZone:getPoint(aZone) aZone.landHeight = land.getHeight({x = zP.x, y= zP.z}) local transitionTime = aZone.transitionTime --17 -- seconds until phosphor lands @@ -404,7 +384,7 @@ function cfxArtilleryZones.update() -- iterate all zones to see if a trigger has changed for idx, aZone in pairs(cfxArtilleryZones.artilleryZones) do - if cfxZones.testZoneFlag(aZone, aZone.artyTriggerFlag, aZone.artyTriggerMethod, "lastTriggerValue") then + if aZone:testZoneFlag(aZone.artyTriggerFlag, aZone.artyTriggerMethod, "lastTriggerValue") then -- a triggered release! cfxArtilleryZones.doFireAt(aZone) -- all from zone vars! if cfxArtilleryZones.verbose then @@ -413,10 +393,10 @@ function cfxArtilleryZones.update() end end - -- old code + --[[-- if aZone.artyTriggerFlag then - local currTriggerVal = cfxZones.getFlagValue(aZone.artyTriggerFlag, aZone) -- trigger.misc.getUserFlag(aZone.artyTriggerFlag) + local currTriggerVal = cfxZones.getFlagValue(aZone.artyTriggerFlag, aZone) if currTriggerVal ~= aZone.lastTriggerValue then -- a triggered release! @@ -430,6 +410,7 @@ function cfxArtilleryZones.update() end end + --]]-- end end @@ -446,8 +427,6 @@ function cfxArtilleryZones.start() -- collect all spawn zones local attrZones = cfxZones.getZonesWithAttributeNamed("artilleryTarget") - -- now create a spawner for all, add them to the spawner updater, and spawn for all zones that are not - -- paused for k, aZone in pairs(attrZones) do cfxArtilleryZones.processArtilleryZone(aZone) -- process attribute and add to zone cfxArtilleryZones.addArtilleryZone(aZone) -- remember it so we can smoke it diff --git a/modules/cfxCargoManager.lua b/modules/cargoManager.lua similarity index 100% rename from modules/cfxCargoManager.lua rename to modules/cargoManager.lua diff --git a/modules/cfxCargoReceiver.lua b/modules/cargoReceiver.lua similarity index 64% rename from modules/cfxCargoReceiver.lua rename to modules/cargoReceiver.lua index 417da1f..b54e49f 100644 --- a/modules/cfxCargoReceiver.lua +++ b/modules/cargoReceiver.lua @@ -1,25 +1,17 @@ cfxCargoReceiver = {} -cfxCargoReceiver.version = "1.2.2" +cfxCargoReceiver.version = "2.0.0" cfxCargoReceiver.ups = 1 -- once a second cfxCargoReceiver.maxDirectionRange = 500 -- in m. distance when cargo manager starts talking to pilots who are carrying that cargo cfxCargoReceiver.requiredLibs = { "dcsCommon", -- always - "cfxPlayer", -- for directions "cfxZones", -- Zones, of course "cfxCargoManager", -- will notify me on a cargo event } --[[-- Version history - - 1.0.0 initial vbersion - - 1.1.0 added flag manipulation options - no negative agl on announcement - silent attribute - - 1.2.0 method - f!, cargoReceived! - - 1.2.1 cargoMethod - - 1.2.2 removed deprecated functions - corrected pollFlag bug (not passing zone along) - distance to receiver is given as distance to zone boundary + - 2.0.0 no more cfxPlayer Dependency + dmlZones, OOP + clean-up CargoReceiver is a zone enhancement you use to be automatically @@ -27,14 +19,7 @@ cfxCargoReceiver.requiredLibs = { It also provides BRA when in range to a cargo receiver *** EXTENDS ZONES - - Callback signature: - cb(event, obj, name, zone) with - - event being string, currently defined: 'deliver' - - obj being the cargo object - - name being cargo object name - - zone in which cargo was dropped (if dropped) - + --]]-- cfxCargoReceiver.receiverZones = {} function cfxCargoReceiver.processReceiverZone(aZone) -- process attribute and add to zone @@ -42,50 +27,22 @@ function cfxCargoReceiver.processReceiverZone(aZone) -- process attribute and ad -- isCargoReceiver flag and we are good aZone.isCargoReceiver = true -- we can add additional processing here - aZone.autoRemove = cfxZones.getBoolFromZoneProperty(aZone, "autoRemove", false) -- maybe add a removeDelay - aZone.removeDelay = cfxZones.getNumberFromZoneProperty(aZone, "removeDelay", 1) + aZone.autoRemove = aZone:getBoolFromZoneProperty("autoRemove", false) -- maybe add a removeDelay + aZone.removeDelay = aZone:getNumberFromZoneProperty("removeDelay", 1) if aZone.removeDelay < 1 then aZone.removeDelay = 1 end - aZone.silent = cfxZones.getBoolFromZoneProperty(aZone, "silent", false) + aZone.silent = aZone:getBoolFromZoneProperty("silent", false) - --trigger.action.outText("+++rcv: recognized receiver zone: " .. aZone.name , 30) - -- same integration as object destruct detector for flags - --[[-- - if cfxZones.hasProperty(aZone, "setFlag") then - aZone.setFlag = cfxZones.getStringFromZoneProperty(aZone, "setFlag", "999") - end - if cfxZones.hasProperty(aZone, "f=1") then - aZone.setFlag = cfxZones.getStringFromZoneProperty(aZone, "f=1", "999") - end - if cfxZones.hasProperty(aZone, "clearFlag") then - aZone.clearFlag = cfxZones.getStringFromZoneProperty(aZone, "clearFlag", "999") - end - if cfxZones.hasProperty(aZone, "f=0") then - aZone.clearFlag = cfxZones.getStringFromZoneProperty(aZone, "f=0", "999") - end - if cfxZones.hasProperty(aZone, "increaseFlag") then - aZone.increaseFlag = cfxZones.getStringFromZoneProperty(aZone, "increaseFlag", "999") - end - if cfxZones.hasProperty(aZone, "f+1") then - aZone.increaseFlag = cfxZones.getStringFromZoneProperty(aZone, "f+1", "999") - end - if cfxZones.hasProperty(aZone, "decreaseFlag") then - aZone.decreaseFlag = cfxZones.getStringFromZoneProperty(aZone, "decreaseFlag", "999") - end - if cfxZones.hasProperty(aZone, "f-1") then - aZone.decreaseFlag = cfxZones.getStringFromZoneProperty(aZone, "f-1", "999") - end - --]]-- -- new method support - aZone.cargoMethod = cfxZones.getStringFromZoneProperty(aZone, "method", "inc") - if cfxZones.hasProperty(aZone, "cargoMethod") then - aZone.cargoMethod = cfxZones.getStringFromZoneProperty(aZone, "cargoMethod", "inc") + aZone.cargoMethod = aZone:getStringFromZoneProperty("method", "inc") + if aZone:hasProperty("cargoMethod") then + aZone.cargoMethod = aZone:getStringFromZoneProperty("cargoMethod", "inc") end - if cfxZones.hasProperty(aZone, "f!") then - aZone.outReceiveFlag = cfxZones.getStringFromZoneProperty(aZone, "f!", "*") - elseif cfxZones.hasProperty(aZone, "cargoReceived!") then - aZone.outReceiveFlag = cfxZones.getStringFromZoneProperty(aZone, "cargoReceived!", "*") + if aZone:hasProperty("f!") then + aZone.outReceiveFlag = aZone:getStringFromZoneProperty("f!", "*") + elseif aZone:hasProperty("cargoReceived!") then + aZone.outReceiveFlag = aZone:getStringFromZoneProperty( "cargoReceived!", "*") end end @@ -134,17 +91,14 @@ end function cfxCargoReceiver.cargoEvent(event, object, name) -- usually called from cargomanager - --trigger.action.outText("Cargo Receiver: event <" .. event .. "> for " .. name, 30) + if not event then return end if event == "grounded" then - --trigger.action.outText("+++rcv: grounded for " .. name, 30) -- this is actually the only one that interests us if not object then - --trigger.action.outText("+++rcv: " .. name .. " has null object", 30) return end - if not object:isExist() then - --trigger.action.outText("+++rcv: " .. name .. " no longer exists", 30) + if not Object.isExist(object) then return end loc = object:getPoint() @@ -156,28 +110,10 @@ function cfxCargoReceiver.cargoEvent(event, object, name) cfxCargoReceiver.invokeCallback("deliver", object, name, aZone) -- set flags as indicated - --[[-- - if aZone.setFlag then - trigger.action.setUserFlag(aZone.setFlag, 1) - end - if aZone.clearFlag then - trigger.action.setUserFlag(aZone.clearFlag, 0) - end - if aZone.increaseFlag then - local val = trigger.misc.getUserFlag(aZone.increaseFlag) + 1 - trigger.action.setUserFlag(aZone.increaseFlag, val) - end - if aZone.decreaseFlag then - local val = trigger.misc.getUserFlag(aZone.decreaseFlag) - 1 - trigger.action.setUserFlag(aZone.decreaseFlag, val) - end - --]]-- if aZone.outReceiveFlag then cfxZones.pollFlag(aZone.outReceiveFlag, aZone.cargoMethod, aZone) end - --trigger.action.outText("+++rcv: " .. name .. " delivered in zone " .. aZone.name, 30) - --trigger.action.outSound("Quest Snare 3.wav") if aZone.autoRemove then -- schedule this for in a few seconds? local args = {} @@ -218,12 +154,12 @@ function cfxCargoReceiver.update() -- this cargo can be talked down. -- find the player unit that is closest to in in hopes -- that that is the one carrying it - local allPlayers = cfxPlayer.getAllPlayers() -- idx by name - for pname, info in pairs(allPlayers) do + local allPlayers = dcsCommon.getAllExistingPlayersAndUnits() -- idx by name + for pname, theUnit in pairs(allPlayers) do -- iterate all player units local closestUnit = nil local minDelta = math.huge - local theUnit = info.unit + --local theUnit = info.unit if theUnit:isExist() then local uPoint = theUnit:getPoint() local currDelta = dcsCommon.distFlat(thePoint, uPoint) @@ -305,4 +241,4 @@ if not cfxCargoReceiver.start() then end -- TODO: config zone for talking down pilots --- TODO: f+/f-/f=1/f=0 \ No newline at end of file +-- detect all pilots in zone (not clear: are all detected or only one) \ No newline at end of file diff --git a/modules/cfxArtillery.lua b/modules/cfxArtillery.lua deleted file mode 100644 index b8dfc96..0000000 --- a/modules/cfxArtillery.lua +++ /dev/null @@ -1,411 +0,0 @@ -cfxArtilleryDemon = {} -cfxArtilleryDemon.version = "1.0.3" --- based on cfx stage demon v 1.0.2 ---[[-- - Version History - 1.0.2 - taken from stageDemon - 1.0.3 - corrected 'messageOut' bug - ---]]-- -cfxArtilleryDemon.messageToAll = true -- set to false if messages should be sent only to the group that set the mark -cfxArtilleryDemon.messageTime = 30 -- how long a message stays on the sceeen - --- cfxArtillery hooks into DCS's mark system to intercept user --- transactions with the mark system and uses that for arty targeting --- used to interactively add ArtilleryZones during gameplay - --- Copyright (c) 2021 by Christian Franz and cf/x AG - -cfxArtilleryDemon.autostart = true -- start automatically - --- whenever you begin a Mark with the string below, it will be taken as a command --- and run through the command parser, stripping the mark, and then splitting --- by blanks -cfxArtilleryDemon.markOfDemon = "-" -- all commands must start with this sequence -cfxArtilleryDemon.splitDelimiter = " " - -cfxArtilleryDemon.unitFilterMethod = nil -- optional user filtering redirection. currently - -- set to allow all users use cfxArtillery -cfxArtilleryDemon.processCommandMethod = nil -- optional initial command processing redirection - -- currently set to cfxArtillery's own processor -cfxArtilleryDemon.commandTable = {} -- key, value pair for command processing per keyword - -- all commands cfxArtillery understands are used as keys and - -- the functions that process them are used as values - -- making the parser a trivial table :) - -cfxArtilleryDemon.demonID = nil -- used only for suspending the event callback - --- unit authorization. You return false to disallow this unit access --- to commands --- simple authorization checks would be to allow only players --- on neutral side, or players in range of location with Lino of sight --- to that point --- -function cfxArtilleryDemon.authorizeAllUnits(event) - -- units/groups that are allowed to give a command can be filtered. - -- return true if the unit/group may give commands - -- cfxArtillery allows anyone to give it commands - return true -end - -function cfxArtilleryDemon.hasMark(theString) - -- check if the string begins with the sequece to identify commands - if not theString then return false end - return theString:find(cfxArtilleryDemon.markOfDemon) == 1 -end - -function cfxArtilleryDemon.splitString(inputstr, sep) - if sep == nil then - sep = "%s" - end - if inputstr == nil then - inputstr = "" - end - - local t={} - for str in string.gmatch(inputstr, "([^"..sep.."]+)") do - table.insert(t, str) - end - return t -end - -function cfxArtilleryDemon.str2num(inVal, default) - if not default then default = 0 end - if not inVal then return default end - if type(inVal) == "number" then return inVal end - local num = nil - if type(inVal) == "string" then num = tonumber(inVal) end - if not num then return default end - return num -end - --- --- output method. can be customized, so we have a central place where we --- can control how output is handled. Is currently outText and outTextToGroup --- -function cfxArtilleryDemon.outMessage(theMessage, args) - if not args then - args = {} - end - local toAll = args.toAll -- will only be true if defined and set to true - - if not args.group then - toAll = true - else - if not args.group:isExist() then - toAll = true - end - end - toAll = toAll or cfxArtilleryDemon.messageToAll - if not toAll then - trigger.action.outTextToGroup(args.group, theMessage, cfxArtilleryDemon.messageTime) - else - trigger.action.outText(theMessage, cfxArtilleryDemon.messageTime) - end -end - --- --- get all player groups - since there is no getGroupByIndex in DCS (yet) --- we simply collect all player groups (since only palyers can place marks) --- and try to match their group ID to the one given by mark -function cfxArtilleryDemon.getAllPayerGroups() - local coalitionSides = {0, 1, 2} -- we currently have neutral, red, blue - local playerGroups = {} - for i=1, #coalitionSides do - local theSide = coalitionSides[i] - -- get all players for this side - local thePlayers = coalition.getPlayers(theSide) - for p=1, #thePlayers do - aPlayerUnit = thePlayers[p] -- docs say this is a unit table, not a person! - if aPlayerUnit:isExist() then - local theGroup = aPlayerUnit:getGroup() - if theGroup:isExist() then - local gID = theGroup:getID() - playerGroups[gID] = theGroup -- multiple players per group results in one group - end - end - end - end - return playerGroups -end - -function cfxArtilleryDemon.retrieveGroupFromEvent(theEvent) --- DEBUG CODE - if theEvent.initiator then - trigger.action.outText("EVENT: initiator set to " .. theEvent.initiator:getName(), 30) - else - trigger.action.outText("EVENT: NO INITIATOR", 30) - end - - trigger.action.outText("EVENT: groupID = " .. theEvent.groupID, 30) - - -- trivial case: initiator is set, and we can access the group - if theEvent.initiator then - if theEvent.initiator:isExist() then - return theEvent.initiator:getGroup() - end - end - - -- ok, bad news: initiator wasn't filled. let's try the fallback: event.groupID - if theEvent.groupID and theEvent.groupID > 0 then - local playerGroups = cfxArtilleryDemon.getAllPayerGroups() - if playerGroups[theEvent.groupID] then - return palyerGroups[theEvent.groupID] - end - end - - -- nope, return nil - return nil -end - --- main hook into DCS. Called whenever a Mark-related event happens --- very simple: look if text begins with special sequence, and if so, --- call the command processor. Note that you can hook your own command --- processor in by changing the value of processCommandMethod -function cfxArtilleryDemon:onEvent(theEvent) - -- while we can hook into any of the three events, - -- we curently only utilize CHANGE Mark - if not (theEvent.id == world.event.S_EVENT_MARK_ADDED) and - not (theEvent.id == world.event.S_EVENT_MARK_CHANGE) and - not (theEvent.id == world.event.S_EVENT_MARK_REMOVED) then - -- not of interest for us, bye bye - return - end - - -- build the messageOut() arg table - local args = {} - args.toAll = cfxArtilleryDemon.toAll --- --- - args.toAll = false -- FORCE GROUPS FOR DEBUGGING OF NEW CODE --- --- - if not args.toAll then - -- we want group-targeted messaging - -- so we need to retrieve the group - local theGroup = cfxArtilleryDemon.retrieveGroupFromEvent(theEvent) - if not theGroup then - args.toAll = true - trigger.action.outText("*** WARNING: cfxArtilleryDemon can't find group for command", 30) - else - args.group = theGroup - end - end - cfxArtilleryDemon.args = args -- copy reference so we can easily use it in messageOut - - -- when we get here, we have a mark event - -- see if the unit filter lets it pass - if not cfxArtilleryDemon.unitFilterMethod(theEvent) then - return -- unit is not allowed to give demon orders. bye bye - end - - if theEvent.id == world.event.S_EVENT_MARK_ADDED then - -- add mark is quite useless for us as we are called when the user clicks, with no - -- text in the description yet. Later abilities may want to use it though - end - - if theEvent.id == world.event.S_EVENT_MARK_CHANGE then - -- when changed, the mark's text is examined for a command - -- if it starts with the 'mark' string ("*" by default) it is processed - -- by the command processor - -- if it is processed succesfully, the mark is immediately removed - -- else an error is displayed and the mark remains. - if cfxArtilleryDemon.hasMark(theEvent.text) then - -- strip the mark - local commandString = theEvent.text:sub(1+cfxArtilleryDemon.markOfDemon:len()) - -- break remainder apart into ... - local commands = cfxArtilleryDemon.splitString(commandString, cfxArtilleryDemon.splitDelimiter) - - -- this is a command. process it and then remove it if it was executed successfully - local success = cfxArtilleryDemon.processCommandMethod(commands, theEvent) - - -- remove this mark after successful execution - if success then - trigger.action.removeMark(theEvent.idx) - cfxArtilleryDemon.outMessage("executed command <" .. commandString .. "> from unit" .. theEvent.initiator:getName(), args) - else - -- we could play some error sound - end - end - end - - if theEvent.id == world.event.S_EVENT_MARK_REMOVED then - end -end - --- --- add / remove commands to/from cfxArtillerys vocabulary --- -function cfxArtilleryDemon.addCommndProcessor(command, processor) - cfxArtilleryDemon.commandTable[command:upper()] = processor -end - -function cfxArtilleryDemon.removeCommandProcessor(command) - cfxArtilleryDemon.commandTable[command:upper()] = nil -end - --- --- process input arguments. Here we simply move them --- up by one. --- -function cfxArtilleryDemon.getArgs(theCommands) - local args = {} - for i=2, #theCommands do - table.insert(args, theCommands[i]) - end - return args -end - --- --- stage demon's main command interpreter. --- magic lies in using the keywords as keys into a --- function table that holds all processing functions --- I wish we had that back in the Oberon days. --- -function cfxArtilleryDemon.executeCommand(theCommands, event) --- trigger.action.outText("executor: *" .. theCommands[1] .. "*", 30) - -- see if theCommands[1] exists in the command table - local cmd = theCommands[1] - local arguments = cfxArtilleryDemon.getArgs(theCommands) - if not cmd then return false end - - -- use the command as index into the table of functions - -- that handle them. - if cfxArtilleryDemon.commandTable[cmd:upper()] then - local theInvoker = cfxArtilleryDemon.commandTable[cmd:upper()] - local success = theInvoker(arguments, event) - return success - else - trigger.action.outText("***error: unknown command <".. cmd .. ">", 30) - return false - end - - return true -end - --- --- SMOKE COMMAND --- - --- known commands and their processors -function cfxArtilleryDemon.smokeColor2Index (theColor) - local color = theColor:lower() - if color == "red" then return 1 end - if color == "white" then return 2 end - if color == "orange" then return 3 end - if color == "blue" then return 4 end - return 0 -end - --- this is the command processing template for your own commands --- when you add a command processor via addCommndProcessor() --- smoke command syntax: '-smoke ' with optional color, color being red, green, blue, white or orange -function cfxArtilleryDemon.processSmokeCommand(args, event) - if not args[1] then args[1] = "red" end -- default to red color - local thePoint = event.pos - thePoint.y = land.getHeight({x = thePoint.x, y = thePoint.z}) +3 -- elevate to ground height - trigger.action.smoke(thePoint, cfxArtilleryDemon.smokeColor2Index(args[1])) - return true -end - --- --- BOOM command --- -function cfxArtilleryDemon.doBoom(args) - --trigger.action.outText("sim shell str=" .. args.strength .. " x=" .. args.point.x .. " z = " .. args.point.z .. " Tdelta = " .. args.tDelta, 30) --- trigger.action.smoke(args.point, 2) - trigger.action.explosion(args.point, args.strength) - -end - -function cfxArtilleryDemon.processBoomCommand(args, event) - if not args[1] then args[1] = "750" end -- default to 750 strength - local transitionTime = 20 -- seconds until shells hit - local shellNum = 17 - local shellBaseStrength = 500 - local shellvariance = 0.2 -- 10% - local center = event.pos -- center of where shells hit - center.y = land.getHeight({x = center.x, y = center.z}) + 3 - -- we now can 'dirty' the position by something. not yet - for i=1, shellNum do - local thePoint = dcsCommon.randomPointInCircle(100, 0, center.x, center.z) - local boomArgs = {} - local strVar = shellBaseStrength * shellvariance - strVar = strVar * (2 * dcsCommon.randomPercent() - 1.0) -- go from -1 to 1 - - boomArgs.strength = shellBaseStrength + strVar - thePoint.y = land.getHeight({x = thePoint.x, y = thePoint.z}) + 1 -- elevate to ground height + 1 - boomArgs.point = thePoint - local timeVar = 5 * (2 * dcsCommon.randomPercent() - 1.0) -- +/- 1.5 seconds - boomArgs.tDelta = timeVar - timer.scheduleFunction(cfxArtilleryDemon.doBoom, boomArgs, timer.getTime() + transitionTime + timeVar) - end - trigger.action.outText("Fire command confirmed. Artillery is firing at your designated co-ordinates.", 30) - trigger.action.smoke(center, 2) -- mark location visually - return true -end - --- --- cfxArtilleryZones interface --- - -function cfxArtilleryDemon.processTargetCommand(args, event) - -- get position - local center = event.pos -- center of where shells hit - center.y = land.getHeight({x = center.x, y = center.z}) - - if not event.initiator then - trigger.action.outText("Target entry aborted: no initiator.", 30) - return true - end - local theUnit = event.initiator - local theGroup = theUnit:getGroup() - local coalition = theGroup:getCoalition() - local spotRange = 3000 - local autoAdd = true - local params = "" - - for idx, param in pairs(args) do - if params == "" then params = ": " - else params = params .. " " - end - params = params .. param - end - - local name = "TgtData".. params .. " (" .. theUnit:getName() .. ")@T+" .. math.floor(timer.getTime()) - -- feed into arty zones - cfxArtilleryZones.createArtilleryZone(name, center, coalition, spotRange, 500, autoAdd) -- 500 is base strength - - trigger.action.outTextForCoalition(coalition, "New ARTY coordinates received from " .. theUnit:getName() .. ", standing by", 30) - return true -end - --- --- cfxArtillery init and start --- - -function cfxArtilleryDemon.init() - cfxArtilleryDemon.unitFilterMethod = cfxArtilleryDemon.authorizeAllUnits - cfxArtilleryDemon.processCommandMethod = cfxArtilleryDemon.executeCommand - - -- now add known commands to interpreter. Add your own commands the same way - cfxArtilleryDemon.addCommndProcessor("smoke", cfxArtilleryDemon.processSmokeCommand) - - cfxArtilleryDemon.addCommndProcessor("bumm", cfxArtilleryDemon.processBoomCommand) - - cfxArtilleryDemon.addCommndProcessor("tgt", - cfxArtilleryDemon.processTargetCommand) - - -- you can add and remove command the same way - trigger.action.outText("cf/x cfx Artillery Demon v" .. cfxArtilleryDemon.version .. " loaded", 30) -end - -function cfxArtilleryDemon.start() - cfxArtilleryDemon.demonID = world.addEventHandler(cfxArtilleryDemon) - trigger.action.outText("cf/x cfxArtilleryDemon v" .. cfxArtilleryDemon.version .. " started", 30) -end - -cfxArtilleryDemon.init() -if cfxArtilleryDemon.autostart then - cfxArtilleryDemon.start() -end \ No newline at end of file diff --git a/modules/cfxMX.lua b/modules/cfxMX.lua index 834c88b..c8269fa 100644 --- a/modules/cfxMX.lua +++ b/modules/cfxMX.lua @@ -1,5 +1,5 @@ cfxMX = {} -cfxMX.version = "1.2.6" +cfxMX.version = "2.0.0" cfxMX.verbose = false --[[-- Mission data decoder. Access to ME-built mission structures @@ -7,26 +7,11 @@ cfxMX.verbose = false Copyright (c) 2022, 2023 by Christian Franz and cf/x AG Version History - 1.0.0 - initial version - 1.0.1 - getStaticFromDCSbyName() - 1.1.0 - getStaticFromDCSbyName also copies groupID when not fetching orig - - on start up collects a cross reference table of all - original group id - - add linkUnit for statics - 1.2.0 - added group name reference table - - added group type reference - - added references for allFixed, allHelo, allGround, allSea, allStatic - 1.2.1 - added countryByName - - added linkByName - 1.2.2 - fixed ctry bug in countryByName - - playerGroupByName - - playerUnitByName - 1.2.3 - groupTypeByName - - groupCoalitionByName - 1.2.4 - playerUnit2Group cross index - 1.2.5 - unitIDbyName index added 1.2.6 - cfxMX.allTrainsByName - train carve-outs for vehicles + 2.0.0 - clean-up + - harmonized with cfxGroups + --]]-- cfxMX.groupNamesByID = {} cfxMX.groupIDbyName = {} @@ -47,13 +32,21 @@ cfxMX.playerGroupByName = {} -- returns data only if a player is in group cfxMX.playerUnitByName = {} -- returns data only if this is a player unit cfxMX.playerUnit2Group = {} -- returns a group data for player units. +cfxMX.groups = {} -- all groups indexed b yname, cfxGroups folded into cfxMX +--[[-- group objects are + { + name= "", + coalition = "" (red, blue, neutral), + coanum = # (0, 1, 2 for neutral, red, blue) + category = "" (helicopter, ship, plane, vehicle, static), + hasPlayer = true/false, + playerUnits = {} (for each player unit in group: name, point, action) + + } + +--]]-- function cfxMX.getGroupFromDCSbyName(aName, fetchOriginal) if not fetchOriginal then fetchOriginal = false end - -- fetch the group description for goup named aName (if exists) - -- returned structure must be parsed for useful information - -- returns data, category, countyID and coalitionID - -- unless fetchOriginal is true, creates a deep clone of - -- group data structure for coa_name_miz, coa_data in pairs(env.mission.coalition) do -- iterate all coalitions local coa_name = coa_name_miz @@ -135,11 +128,6 @@ function cfxMX.getStaticFromDCSbyName(aName, fetchOriginal) if type(cntry_data) == 'table' then -- filter strings .id and .name for obj_type_name, obj_type_data in pairs(cntry_data) do if obj_type_name == "static" --- obj_type_name == "helicopter" or --- obj_type_name == "ship" or --- obj_type_name == "plane" or --- obj_type_name == "vehicle" or --- obj_type_name == "static" then -- (only look at statics) local category = obj_type_name if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's at least one static in group! @@ -149,7 +137,7 @@ function cfxMX.getStaticFromDCSbyName(aName, fetchOriginal) 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 if linkUnit then - --trigger.action.outText("MX: found missing link to " .. linkUnit .. " in " .. group_data.name, 30) + end end @@ -207,7 +195,7 @@ function cfxMX.createCrossReferences() obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" -- what about "cargo"? - -- not that trains appear as 'vehicle' + -- note that trains appear as 'vehicle' then -- (so it's not id or name) local category = obj_type_name if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's at least one group! @@ -252,6 +240,12 @@ function cfxMX.createCrossReferences() end -- now iterate all units in this group -- for unit xref like player info and ID + local hasPlayer = false + local playerUnits = {} + local groupName = group_data.name + if env.mission.version > 7 then -- translate raw to actual + groupName = env.getValueDictByKey(groupName) + end for unit_num, unit_data in pairs(group_data.units) do if unit_data.skill then if unit_data.skill == "Client" or unit_data.skill == "Player" then @@ -259,10 +253,38 @@ function cfxMX.createCrossReferences() cfxMX.playerUnitByName[unit_data.name] = unit_data cfxMX.playerGroupByName[aName] = group_data -- inefficient, but works cfxMX.playerUnit2Group[unit_data.name] = group_data + + hasPlayer = true + local playerData = {} + playerData.name = unit_data.name + playerData.point = {} + playerData.point.x = unit_data.x + playerData.point.y = 0 + playerData.point.z = unit_data.y + playerData.action = "none" -- default + + -- access initial waypoint data by 'reaching up' + -- into group data and extract route.points[1] + if group_data.route and group_data.route.points and (#group_data.route.points > 0) then + playerData.action = group_data.route.points[1].action + end + table.insert(playerUnits, playerData) + end -- if unit skill client end -- if has skill cfxMX.unitIDbyName[unit_data.name] = unit_data.unitId end -- for all units + + local entry = {} + entry.name = groupName + entry.coalition = coa_name + entry.coaNum = coaNum + entry.category = category + entry.hasPlayer = hasPlayer + entry.playerUnits = playerUnits + -- add to db + cfxMX.groups[groupName] = entry + end -- for all groups end --if has category data end --if plane, helo etc... category @@ -274,6 +296,31 @@ function cfxMX.createCrossReferences() end --for all coalitions in mission end + +-- return all groups that can have players in them +-- includes groups that currently are not or not anymore alive +function cfxMX.getPlayerGroup() + local playerGroups = {} + for gName, gData in pairs (cfxMX.groups) do + if gData.hasPlayer then + table.insert(playerGroups, gData) + end + end + return playerGroups +end + +-- return all group names that can have players in them +-- includes groups that currently are not or not anymore alive +function cfxMX.getPlayerGroupNames() + local playerGroups = {} + for gName, gData in pairs (cfxMX.groups) do + if gData.hasPlayer then + table.insert(playerGroups, gName) + end + end + return playerGroups +end + function cfxMX.catText2ID(inText) local outCat = 0 -- airplane local c = inText:lower() @@ -283,7 +330,7 @@ function cfxMX.catText2ID(inText) if c == "vehicle" then outCat = 2 end if c == "train" then outCat = 4 end if c == "static" then outCat = -1 end - --trigger.action.outText("cat2text: in <" .. inText .. "> out <" .. outCat .. ">", 30) + return outCat end @@ -292,6 +339,7 @@ function cfxMX.start() if cfxMX.verbose then trigger.action.outText("cfxMX: "..#cfxMX.groupNamesByID .. " groups processed successfully", 30) end + trigger.action.outText("cfxMX v." .. cfxMX.version .. " started.", 30) end -- start diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index d352bab..f217452 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "4.0.10" +cfxZones.version = "4.1.1" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -9,31 +9,6 @@ cfxZones.version = "4.0.10" -- --[[-- VERSION HISTORY -- 3.0.0 - support for DCS 2.8 linkUnit attribute, integration with - linedUnit and warning. - - initZoneVerbosity() -- 3.0.1 - updateMovingZones() better tracks linked units by name -- 3.0.2 - maxRadius for all zones, only differs from radius in polyZones - - re-factoring zone-base string processing from messenger module - - new processStringWildcards() that does almost all that messenger can -- 3.0.3 - new getLinkedUnit() -- 3.0.4 - new createRandomPointOnZoneBoundary() -- 3.0.5 - getPositiveRangeFromZoneProperty() now also supports upper bound (optional) -- 3.0.6 - new createSimplePolyZone() - - new createSimpleQuadZone() -- 3.0.7 - getPoint() can also get land y when passing true as second param -- 3.0.8 - new cfxZones.pointInOneOfZones(thePoint, zoneArray, useOrig) -- 3.0.9 - new getFlareColorStringFromZoneProperty() -- 3.1.0 - new getRGBVectorFromZoneProperty() - new getRGBAVectorFromZoneProperty() -- 3.1.1 - getRGBAVectorFromZoneProperty now supports #RRGGBBAA and #RRGGBB format - - owner for all, default 0 -- 3.1.2 - getAllZoneProperties has numbersOnly option -- 3.1.3 - new numberArrayFromString() - - new declutterZone() - - new getZoneVolume() - - offsetZone also updates zone bounds when moving zones - - corrected bug in calculateZoneBounds() - 4.0.0 - dmlZone OOP API started - code revision / refactoring - moved createPoint and copxPoint to dcsCommon, added bridging code @@ -66,6 +41,9 @@ cfxZones.version = "4.0.10" - createPolyZone now correctly inits dcsOrigin - createCircleZone noew correctly inits dcsOrigin - 4.0.10 - getBoolFromZoneProperty also supports "on" (=true) and "off" (=false) +- 4.1.0 - getBoolFromZoneProperty 'on/off' support for dml variant as well +- 4.1.1 - evalRemainder() updates + --]]-- -- @@ -99,40 +77,6 @@ cfxZones.ups = 1 -- updates per second. updates moving zones cfxZones.zones = {} -- these are the zone as retrieved from the mission. -- ALWAYS USE THESE, NEVER DCS's ZONES!!!! --- a zone has the following attributes --- x, z -- coordinate of center. note they have correct x, 0, z coordinates so no y-->z mapping --- radius (zero if quad zone) --- isCircle (true if quad zone) --- poly the quad coords are in the poly attribute and are a --- 1..n, wound counter-clockwise as (currently) in DCS: --- lower left, lower right upper left, upper right, all coords are x, 0, z --- bounds - contain the AABB coords for the zone: ul (upper left), ur, ll (lower left), lr --- for both circle and poly, all (x, 0, z) - --- zones can carry information in their names that can get processed into attributes --- use --- zones can also carry information in their 'properties' tag that ME allows to --- edit. cfxZones provides an easy method to access these properties --- - getZoneProperty (returns as string) --- - getMinMaxFromZoneProperty --- - getBoolFromZoneProperty --- - getNumberFromZoneProperty - - --- SUPPORTED PROPERTIES --- - "linkedUnit" - zone moves with unit of that name. must be exact match --- can be combined with other attributes that extend (e.g. scar manager and --- limited pilots/airframes --- - --- --- readZonesFromDCS is executed exactly once at the beginning --- from then on, use only the cfxZones.zones table --- WARNING: cfxZones is NOT case-sensitive. All zone names are --- indexed by upper case. If you have two zones with same name but --- different case, one will be replaced --- - function cfxZones.readFromDCS(clearfirst) if (clearfirst) then cfxZones.zones = {} @@ -617,9 +561,7 @@ function cfxZones.createRandomZoneInZone(name, inZone, targetRadius, entirelyIns -- if entirelyInside is false, only the zone's center is guaranteed to be inside -- inZone. -- entirelyInside is not guaranteed for polyzones - --- trigger.action.outText("Zones: creating rZiZ with tr = " .. targetRadius .. " for " .. inZone.name .. " that as r = " .. inZone.radius, 10) - + if inZone.isCircle then local sourceRadius = inZone.radius if entirelyInside and targetRadius > sourceRadius then targetRadius = sourceRadius end @@ -1364,11 +1306,11 @@ end -- creating units in a zone -function cfxZones.createGroundUnitsInZoneForCoalition (theCoalition, groupName, theZone, theUnits, formation, heading) +function cfxZones.createGroundUnitsInZoneForCoalition (theCoalition, groupName, theZone, theUnits, formation, heading, liveries) -- theUnits can be string or table of string if not groupName then groupName = "G_"..theZone.name end -- group name will be taken from zone name and prependend with "G_" - local theGroup = dcsCommon.createGroundGroupWithUnits(groupName, theUnits, theZone.radius, nil, formation) + local theGroup = dcsCommon.createGroundGroupWithUnits(groupName, theUnits, theZone.radius, nil, formation, nil, liveries) -- turn the entire formation to heading if (not heading) then heading = 0 end @@ -1380,7 +1322,6 @@ function cfxZones.createGroundUnitsInZoneForCoalition (theCoalition, groupName, theZone.point.x, theZone.point.z) -- watchit: Z!!! - -- create the group in the world and return it -- first we need to translate the coalition to a legal -- country. we use UN for neutral, cjtf for red and blue @@ -1446,7 +1387,7 @@ function cfxZones.unPulseFlag(args) cfxZones.setFlagValue(theFlag, newVal, theZone) end -function cfxZones.evalRemainder(remainder) +function cfxZones.evalRemainder(remainder, theZone) local rNum = tonumber(remainder) if not rNum then -- we use remainder as name for flag @@ -1475,6 +1416,10 @@ function cfxZones.evalRemainder(remainder) return rNum end +function dmlZone:evalRemainder(remainder) + return cfxZones.evalRemainder(remainder, self) +end + function cfxZones.doPollFlag(theFlag, method, theZone) -- no OOP equivalent -- WARNING: -- if method is a number string, it will be interpreted as follows: @@ -2381,7 +2326,7 @@ function cfxZones.getBoolFromZoneProperty(theZone, theProperty, defaultVal) if defaultVal == false then -- only go true if exact match to yes or true theBool = false - theBool = (p == 'true') or (p == 'yes') or (p == "1") or (p == "on") + theBool = (p == 'true') or (p == 'yes') or (p == "1") or (p == 'on') return theBool end @@ -2407,13 +2352,13 @@ function dmlZone:getBoolFromZoneProperty(theProperty, defaultVal) if defaultVal == false then -- only go true if exact match to yes or true theBool = false - theBool = (p == 'true') or (p == 'yes') or p == "1" + theBool = (p == 'true') or (p == 'yes') or (p == "1") or (p=="on") return theBool end local theBool = true -- only go false if exactly no or false or "0" - theBool = (p ~= 'false') and (p ~= 'no') and (p ~= "0") + theBool = (p ~= 'false') and (p ~= 'no') and (p ~= "0") and (p ~= "off") return theBool end @@ -3576,8 +3521,6 @@ function cfxZones.init() -- pre-read zone owner for all zones -- much like verbose, all zones have owner --- local pZones = cfxZones.zonesWithProperty("owner") --- for n, aZone in pairs(pZones) do for n, aZone in pairs(cfxZones.zones) do aZone.owner = cfxZones.getCoalitionFromZoneProperty(aZone, "owner", 0) end diff --git a/modules/cfxmon.lua b/modules/cfxmon.lua deleted file mode 100644 index 1610d3d..0000000 --- a/modules/cfxmon.lua +++ /dev/null @@ -1,140 +0,0 @@ -cfxmon = {} -cfxmon.version = "1.0.1" -cfxmon.delay = 30 -- seconds for display ---[[-- - Version History - 1.0.0 - initial version - 1.0.1 - better guard for even.initiator to check if unit exists - - and handle static objcects and other non-grouped objects - -cfxmon is a monitor for all cfx events and callbacks -use monConfig to tell cfxmon which events and callbacks -to monitor. a Property with "no" or "false" will turn -that monitor OFF, else it will stay on - -supported modules if loaded - dcsCommon - cfxPlayer - cfxGroundTroops - cfxObjectDestructDetector - cfxSpawnZones - ---]]-- - --- --- CALLBACKS --- --- dcsCommon Callbacks -function cfxmon.pre(event) - trigger.action.outText("***mon - dcsPre: " .. event.id .. " (" .. dcsCommon.event2text(event.id) .. ")", cfxmon.delay) - return true -end - -function cfxmon.post(event) - trigger.action.outText("***mon - dcsPost: " .. event.id .. " (" .. dcsCommon.event2text(event.id) .. ")", cfxmon.delay) -end - -function cfxmon.rejected(event) - trigger.action.outText("***mon - dcsReject: " .. event.id .. " (" .. dcsCommon.event2text(event.id) .. ")", cfxmon.delay) -end - -function cfxmon.dcsCB(event) -- callback - local initiatorStat = "" - if event.initiator and Unit.isExist(event.initiator) then - local theUnit = event.initiator - -- we assume it is unit, but it can be a static object, - -- and may not have getGroup implemented! - if theUnit.getGroup then - local theGroup = theUnit:getGroup() - - local theGroupName = "" - if theGroup then theGroupName = theGroup:getName() end - initiatorStat = ", for " .. theUnit:getName() - initiatorStat = initiatorStat .. " of " .. theGroupName - else - initiatorStat = ", non-unit (static?) " .. theUnit:getName() - end - else - initiatorStat = ", NO Initiator" - end - trigger.action.outText("***mon - dcsMAIN: " .. event.id .. " (" .. dcsCommon.event2text(event.id) .. ")" .. initiatorStat, cfxmon.delay) -end - --- cfxPlayer callback -function cfxmon.playerEventCB(evType, description, info, data) - trigger.action.outText("***mon - cfxPlayer: ".. evType ..": <" .. description .. ">", cfxmon.delay) -end - --- cfxGroundTroops callback -function cfxmon.groundTroopsCB(reason, theGroup, orders, data) - trigger.action.outText("***mon - groundTroops: ".. reason ..": for group <" .. theGroup:getName() .. "> with orders " .. orders, cfxmon.delay) -end - --- object destruct callbacks -function cfxmon.oDestructCB(zone, ObjectID, name) - trigger.action.outText("***mon - object destroyed: ".. ObjectID .." named <" .. name .. "> in zone " .. zone.name, cfxmon.delay) -end - --- spawner callback -function cfxmon.spawnZoneCB(reason, theGroup, theSpawner) - local gName = "" - if theGroup then gName = theGroup:getName() end - trigger.action.outText("***mon - Spawner: ".. reason .." group <" .. gName .. "> in zone " .. theSpawner.name, cfxmon.delay) -end - --- READ CONFIG AND SUBSCRIBE -function cfxmon.start () - local theZone = cfxZones.getZoneByName("monConfig") - if not theZone then - trigger.action.outText("***mon: WARNING: NO config, defaulting", cfxmon.delay) - theZone = cfxZones.createSimpleZone("MONCONFIG") - end - - -- own config - cfxmon.delay = cfxZones.getNumberFromZoneProperty(theZone, "delay", 30) - trigger.action.outText("!!!mon: Delay is set to: " .. cfxmon.delay .. "seconds", 50) - - -- dcsCommon - if cfxZones.getBoolFromZoneProperty(theZone, "dcsCommon", true) then - -- subscribe to dcs event handlers - -- note we have all, but only connect the main - dcsCommon.addEventHandler(cfxmon.dcsCB) -- we only connect one - trigger.action.outText("!!!mon: +dcsCommon", cfxmon.delay) - else - trigger.action.outText("***mon: -dcsCommon", cfxmon.delay) - end - - -- cfxPlayer - if cfxPlayer and cfxZones.getBoolFromZoneProperty(theZone, "cfxPlayer", true) then - cfxPlayer.addMonitor(cfxmon.playerEventCB) - trigger.action.outText("!!!mon: +cfxPlayer", cfxmon.delay) - else - trigger.action.outText("***mon: -cfxPlayer", cfxmon.delay) - end - - -- cfxGroundTroops - if cfxGroundTroops and cfxZones.getBoolFromZoneProperty(theZone, "cfxGroundTroops", true) then - cfxGroundTroops.addTroopsCallback(cfxmon.groundTroopsCB) - trigger.action.outText("!!!mon: +cfxGroundTroops", cfxmon.delay) - else - trigger.action.outText("***mon: -cfxGroundTroops", cfxmon.delay) - end - - -- objectDestructZones - if cfxObjectDestructDetector and cfxZones.getBoolFromZoneProperty(theZone, "cfxObjectDestructDetector", true) then - cfxObjectDestructDetector.addCallback(cfxmon.oDestructCB) - trigger.action.outText("!!!mon: +cfxObjectDestructDetector", cfxmon.delay) - else - trigger.action.outText("***mon: -cfxObjectDestructDetector", cfxmon.delay) - end - - -- spawnZones - if cfxSpawnZones and cfxZones.getBoolFromZoneProperty(theZone, "cfxSpawnZones", true) then - cfxSpawnZones.addCallback(cfxmon.spawnZoneCB) - trigger.action.outText("!!!mon: +cfxSpawnZones", cfxmon.delay) - else - trigger.action.outText("***mon: -cfxSpawnZones", cfxmon.delay) - end -end - -cfxmon.start() diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index 469ccd7..5daabb3 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -1,5 +1,5 @@ cloneZones = {} -cloneZones.version = "1.9.1" +cloneZones.version = "2.0.1" cloneZones.verbose = false cloneZones.requiredLibs = { "dcsCommon", -- always @@ -27,79 +27,9 @@ cloneZones.respawnOnGroupID = true --[[-- Clones Groups from ME mission data - Copyright (c) 2022 by Christian Franz and cf/x AG + Copyright (c) 2022-2024 by Christian Franz and cf/x AG Version History - 1.0.0 - initial version - 1.0.1 - preWipe attribute - 1.1.0 - support for static objects - - despawn? attribute - 1.1.1 - despawnAll: isExist guard - - map in? to f? - 1.2.0 - Lua API integration: callbacks - - groupXlate struct - - unitXlate struct - - resolveReferences - - getGroupsInZone rewritten for data - - static resolve - - linkUnit resolve - - clone? synonym - - empty! and method attributes - 1.3.0 - DML flag upgrade - 1.3.1 - groupTracker interface - - trackWith: attribute - 1.4.0 - Watchflags - 1.4.1 - trackWith: accepts list of trackers - 1.4.2 - onstart delays for 0.1 s to prevent static stacking - - turn bug for statics (bug in dcsCommon, resolved) - 1.4.3 - embark/disembark now works with cloners - 1.4.4 - removed some debugging verbosity - 1.4.5 - randomizeLoc, rndLoc keyword - - cargo manager integration - pass cargo objects when present - 1.4.6 - removed some verbosity for spawned aircraft with airfields on their routes - 1.4.7 - DML watchflag and DML Flag polish, method-->cloneMethod - 1.4.8 - added 'wipe?' synonym - 1.4.9 - onRoad option - - rndHeading option - 1.5.0 - persistence - 1.5.1 - fixed static data cloning bug (load & save) - 1.5.2 - fixed bug in trackWith: referencing wrong cloner - 1.5.3 - centerOnly/wholeGroups attribute for rndLoc, rndHeading and onRoad - 1.5.4 - parking for aircraft processing when cloning from template - 1.5.5 - removed some verbosity - 1.6.0 - fixed issues with cloning for zones with linked units - - cloning with useHeading - - major declutter - 1.6.1 - removed some verbosity when not rotating routes - - updateTaskLocations () - - cloning groups now also adjusts tasks like search and engage in zone - - cloning with rndLoc supports polygons - - corrected rndLoc without centerOnly to not include individual offsets - - ensure support of recovery tanker resolve cloned group - 1.6.2 - optimization to hasLiveUnits() - 1.6.3 - removed verbosity bug with rndLoc - uniqueNameGroupData has provisions for naming scheme - new uniqueNameStaticData() for naming scheme - 1.6.4 - uniqueCounter is now configurable via config zone - new lclUniqueCounter config, also added to persistence - new globalCount - 1.7.0 - wildcard "*" for masterOwner - - identical attribute: makes identical ID and name for unit and group as template - - new sameIDUnitData() - - nameScheme attribute: allow parametric names - - , , wildcards - - , , , wildcards - - identical=true overrides nameScheme - - masterOwner "*" convenience shortcut - 1.7.1 - useDelicates handOff for delicates - - forcedRespawn passes zone instead of verbose - 1.7.2 - onPerimeter attribute - 1.7.3 - declutter option - 1.8.0 - OOP cfxZones - - removed "empty+1" as planned - - upgraded config zone parsing - 1.8.1 - clone zone definition now supports quads - 1.8.2 - on pre-wipe, delay respawn by 0.5s to avoid 'dropping' statics 1.9.0 - minor clean-up for synonyms - spawnWithSpawner alias for HeloTroops etc requestable SPAWN - requestable attribute @@ -107,6 +37,9 @@ cloneZones.respawnOnGroupID = true - cloner collects all types used - groupScheme attribute 1.9.1 - useAI attribute + 2.0.0 - clean-up + 2.0.1 - improved empty! logic to account for deferred spawn + when pre-wipe is active --]]-- -- @@ -136,16 +69,6 @@ function cloneZones.addCallback(theCallback) table.insert(cloneZones.callbacks, theCallback) end --- reasons for callback --- "will despawn group" - args is the group about to be despawned --- "did spawn group" -- args is group that was spawned --- "will despawn static" --- "did spawn static" --- "spawned" -- completed spawn cycle. args contains .groups and .statics spawned --- "empty" -- all spawns have been killed, args is empty --- "wiped" -- preWipe executed --- "", 30) if not theZone.allTypes[theType] then theZone.allTypes[theType] = 1 -- first one else @@ -318,11 +237,8 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" theZone.cooldown = theZone:getNumberFromZoneProperty("cooldown", -1) -- anything > 0 activates cd theZone.lastSpawnTimeStamp = -10000 - theZone.onStart = theZone:getBoolFromZoneProperty("onStart", false) - theZone.moveRoute = theZone:getBoolFromZoneProperty("moveRoute", false) - theZone.preWipe = theZone:getBoolFromZoneProperty("preWipe", false) if theZone:hasProperty("empty!") then @@ -383,7 +299,6 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" theZone.rndHeading = theZone:getBoolFromZoneProperty("rndHeading", false) theZone.onRoad = theZone:getBoolFromZoneProperty("onRoad", false) - theZone.onPerimeter = theZone:getBoolFromZoneProperty("onPerimeter", false) -- check for name scheme and / or identical @@ -418,9 +333,7 @@ function cloneZones.despawnAll(theZone) if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: despawn all - wiping zone <" .. theZone.name .. ">", 30) end - for idx, aGroup in pairs(theZone.mySpawns) do - --trigger.action.outText("++clnZ: despawn all " .. aGroup.name, 30) - + for idx, aGroup in pairs(theZone.mySpawns) do if aGroup:isExist() then if theZone.verbose then trigger.action.outText("+++clnZ: will destroy <" .. aGroup:getName() .. ">", 30) @@ -638,9 +551,6 @@ function cloneZones.nameFromSchema(schema, inName, theZone, sourceName, i) pos = string.find(outName, "") end - --if theZone.verbose then - -- trigger.action.outText("+++cln: schema [" .. schema .. "] for unit <" .. inName .. "> in zone <" .. theZone.name .. "> returns <" .. outName .. ">, iter = " .. iter, 30) - --end return outName, iter end @@ -866,7 +776,6 @@ function cloneZones.resolveWPReferences(rawData, theZone, dataTable) for grpIdx, gID in pairs(embarkers) do local resolvedID = cloneZones.resolveGroupID(gID, rawData, dataTable, "embark") table.insert(newEmbarkers, resolvedID) - --trigger.action.outText("+++clnZ: resolved embark group id <" .. gID .. "> to <" .. resolvedID .. ">", 30) end -- replace old with new table taskData.params.groupsForEmbarking = newEmbarkers @@ -884,7 +793,6 @@ function cloneZones.resolveWPReferences(rawData, theZone, dataTable) -- translate old to new local resolvedID = cloneZones.resolveGroupID(gID, rawData, dataTable, "embark") table.insert(newEmbarkers, resolvedID) - --trigger.action.outText("+++clnZ: resolved distribute unit/group id <" .. aUnit .. "/" .. gID .. "> to <".. newUnit .. "/" .. resolvedID .. ">", 30) end -- store this as new group for -- translated transportID @@ -892,7 +800,6 @@ function cloneZones.resolveWPReferences(rawData, theZone, dataTable) end -- replace old distribution with new taskData.params.distribution = newDist - --trigger.action.outText("+++clnZ: rebuilt distribution", 30) end -- resolve selectedTransport unit reference @@ -900,7 +807,6 @@ function cloneZones.resolveWPReferences(rawData, theZone, dataTable) local tID = taskData.params.selectedTransport local newTID = cloneZones.resolveUnitID(tID, rawData, dataTable, "transportID") taskData.params.selectedTransport = newTID - --trigger.action.outText("+++clnZ: resolved selected transport <" .. tID .. "> to <" .. newTID .. ">", 30) end -- note: we may need to process x and y as well @@ -932,7 +838,6 @@ function cloneZones.resolveReferences(theZone, dataTable) -- when an action refers to another group, we check if -- the group referred to is also a clone, and update -- the reference to the newest incardnation - for idx, rawData in pairs(dataTable) do -- resolve references in waypoints cloneZones.resolveWPReferences(rawData, theZone, dataTable) @@ -946,7 +851,6 @@ function cloneZones.handoffTracking(theGroup, theZone) return end local trackerName = theZone.trackWith - --if trackerName == "*" then trackerName = theZone.name end -- now assemble a list of all trackers if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: clone pass-off: " .. trackerName, 30) @@ -1065,8 +969,6 @@ function cloneZones.forcedRespawn(args) if newGroupID == theData.CZTargetID then if verbose then trigger.action.outText("GOOD REPLACEMENT new ID <" .. newGroupID .. "> matches target <" .. theData.CZTargetID .. "> for <" .. theData.name .. ">", 30) - -- we can now remove the former group - -- and replace it with the new one trigger.action.outText("will replace table entry at <" .. pos .. "> with new group", 30) end spawnedGroups[pos] = theGroup @@ -1104,7 +1006,7 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) local newCenter = spawnZone:getPoint() -- includes zone following updates local oCenter = theZone:getDCSOrigin() -- get original coords on map for cloning offsets -- calculate zoneDelta, is added to all vectors - local zoneDelta = dcsCommon.vSub(newCenter, theZone.origin) -- oCenter) --theZone.origin) + local zoneDelta = dcsCommon.vSub(newCenter, theZone.origin) -- precalc turn value for linked rotation local dHeading = 0 -- for linked zones @@ -1280,7 +1182,6 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) -- make group, unit[1] and route point [1] all match up if rawData.route and rawData.units[1] then - -- trigger.action.outText("matching route point 1 and group with unit 1", 30) rawData.route.points[1].x = rawData.units[1].x rawData.route.points[1].y = rawData.units[1].y rawData.x = rawData.units[1].x @@ -1439,7 +1340,6 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) if not spawnZone.identical then -- make sure static name is unique and remember original cloneZones.uniqueNameStaticData(rawData, spawnZone, theZone.name) - --rawData.name = dcsCommon.uuid(rawData.name) rawData.unitId = cloneZones.uniqueID() end rawData.CZTargetID = rawData.unitId @@ -1529,7 +1429,6 @@ function cloneZones.turnOffAI(args) local theGroup = args[1] local theController = theGroup:getController() theController:setOnOff(false) --- trigger.action.outText("turned off AI for group <" .. theGroup:getName() .. "> ", 30) end -- retro-fit for helo troops and others to provide 'requestable' support @@ -1721,7 +1620,6 @@ function cloneZones.getRequestableClonersInRange(aPoint, aRange, aSide) if aSide ~= 0 then -- check if side is correct for owned zone local resolved = cloneZones.resolveOwningCoalition(aZone) - --if resolved ~= 0 and resolved ~= aSide then if resolved == 0 or resolved ~= aSide then -- failed ownership test. must match and not be zero hasMatch = false @@ -1749,7 +1647,7 @@ function cloneZones.update() for idx, aZone in pairs(cloneZones.cloners) do -- see if deSpawn was pulled. Must run before spawn if aZone.deSpawnFlag then - local currTriggerVal = aZone:getFlagValue(aZone.deSpawnFlag) -- trigger.misc.getUserFlag(aZone.deSpawnFlag) + local currTriggerVal = aZone:getFlagValue(aZone.deSpawnFlag) if currTriggerVal ~= aZone.lastDeSpawnValue then if cloneZones.verbose or aZone.verbose then trigger.action.outText("+++clnZ: DEspawn triggered for <" .. aZone.name .. ">", 30) @@ -1760,20 +1658,23 @@ function cloneZones.update() end -- see if we got spawn? command + local willSpawn = false -- init to false. if aZone:testZoneFlag(aZone.spawnFlag, aZone.cloneTriggerMethod, "lastSpawnValue") then - if cloneZones.verbose then + if cloneZones.verbose or aZone.verbose then trigger.action.outText("+++clnZ: spawn triggered for <" .. aZone.name .. ">", 30) end cloneZones.spawnWithCloner(aZone) + willSpawn = true -- in case prewipe, we delay + -- can mess with empty, so we tell empty to skip end -- empty handling local isEmpty = cloneZones.countLiveUnits(aZone) < 1 and aZone.hasClones - if isEmpty then + if isEmpty and (willSpawn == false) then -- see if we need to bang a flag if aZone.emptyBangFlag then aZone:pollFlag(aZone.emptyBangFlag, aZone.cloneMethod) - if cloneZones.verbose then + if cloneZones.verbose or aZone.verbose then trigger.action.outText("+++clnZ: bang! on " .. aZone.emptyBangFlag, 30) end end @@ -1983,7 +1884,6 @@ function cloneZones.loadData() trigger.action.outText("+++clnZ: linked static <" .. oName .. "> to unit <" .. newStatic.linkUnit .. ">", 30) end local cty = newStatic.cty --- local cat = staticData.cat -- spawn new one, replacing same.named old, dead if required gStatic = coalition.addStaticObject(cty, newStatic) @@ -2004,7 +1904,7 @@ function cloneZones.loadData() for cName, cData in pairs(allCloners) do local theCloner = cloneZones.getCloneZoneByName(cName) if theCloner then - theCloner.isStarted = true -- ALWAYS TRUE WHEN WE COME HERE! cData.isStarted + theCloner.isStarted = true -- init myUniqueCounter if it exists if cData.myUniqueCounter then theCloner.myUniqueCounter = cData.myUniqueCounter @@ -2094,10 +1994,7 @@ function cloneZones.start() cloneZones.readConfigZone() -- process cloner Zones - local attrZones = cfxZones.getZonesWithAttributeNamed("cloner") - - -- now create an rnd gen for each one and add them - -- to our watchlist + local attrZones = cfxZones.getZonesWithAttributeNamed("cloner") for k, aZone in pairs(attrZones) do cloneZones.createClonerWithZone(aZone) -- process attribute and add to zone cloneZones.addCloneZone(aZone) diff --git a/modules/cfxCommander.lua b/modules/commander.lua similarity index 100% rename from modules/cfxCommander.lua rename to modules/commander.lua diff --git a/modules/countDown.lua b/modules/countDown.lua index a0d4fd6..ed8d7e3 100644 --- a/modules/countDown.lua +++ b/modules/countDown.lua @@ -1,5 +1,5 @@ countDown = {} -countDown.version = "1.3.2" +countDown.version = "2.0.0" countDown.verbose = false countDown.ups = 1 countDown.requiredLibs = { @@ -9,26 +9,14 @@ countDown.requiredLibs = { --[[-- count down on flags to generate new signal on out - Copyright (c) 2022, 2023 by Christian Franz and cf/x AG + Copyright (c) 2022 - 2024 by Christian Franz and cf/x AG Version History - 1.0.0 - initial version - 1.1.0 - Lua interface: callbacks - - corrected verbose (erroneously always suppressed) - - triggerFlag --> triggerCountFlag - 1.1.1 - corrected bug in invokeCallback - 1.2.0 - DML Flags - - counterOut! - - ups config - 1.2.1 - disableCounter? - 1.3.0 - DML & Watchflags upgrade - - method --> ctdwnMethod - 1.3.1 - clock? synonym - - new reset? input - - improved verbosity - - zone-local verbosity - 1.3.2 - enableCounter? to balande disableCounter? - + 2.0.0 - dmlZones, OOP upgrade + counterOut! --> counterOut# + output method defaults to "inc" + better config parsing + cleanup --]]-- countDown.counters = {} @@ -77,7 +65,7 @@ end -- function countDown.createCountDownWithZone(theZone) -- start val - a range - theZone.startMinVal, theZone.startMaxVal = cfxZones.getPositiveRangeFromZoneProperty(theZone, "countDown", 1) -- we know this exists + theZone.startMinVal, theZone.startMaxVal = theZone:getPositiveRangeFromZoneProperty("countDown", 1) -- we know this exists theZone.currVal = dcsCommon.randomBetween(theZone.startMinVal, theZone.startMaxVal) if countDown.verbose then @@ -85,32 +73,32 @@ function countDown.createCountDownWithZone(theZone) end -- loop - theZone.loop = cfxZones.getBoolFromZoneProperty(theZone, "loop", false) + theZone.loop = theZone:getBoolFromZoneProperty("loop", false) -- extend after zero - theZone.belowZero = cfxZones.getBoolFromZoneProperty(theZone, "belowZero", false) + theZone.belowZero = theZone:getBoolFromZoneProperty("belowZero", false) -- out method - theZone.ctdwnMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "flip") - if cfxZones.hasProperty(theZone, "ctdwnMethod") then - theZone.ctdwnMethod = cfxZones.getStringFromZoneProperty(theZone, "ctdwnMethod", "flip") + theZone.ctdwnMethod = theZone:getStringFromZoneProperty("method", "inc") + if theZone:hasProperty("ctdwnMethod") then + theZone.ctdwnMethod = theZone:getStringFromZoneProperty( "ctdwnMethod", "inc") end -- triggerMethod for inputs - theZone.ctdwnTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") + theZone.ctdwnTriggerMethod = theZone:getStringFromZoneProperty( "triggerMethod", "change") - if cfxZones.hasProperty(theZone, "ctdwnTriggerMethod") then - theZone.ctdwnTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "ctdwnTriggerMethod", "change") + if theZone:hasProperty("ctdwnTriggerMethod") then + theZone.ctdwnTriggerMethod = theZone:getStringFromZoneProperty("ctdwnTriggerMethod", "change") end -- trigger flag "count" / "start?" - if cfxZones.hasProperty(theZone, "count?") then - theZone.triggerCountFlag = cfxZones.getStringFromZoneProperty(theZone, "count?", "") - elseif cfxZones.hasProperty(theZone, "clock?") then - theZone.triggerCountFlag = cfxZones.getStringFromZoneProperty(theZone, "clock?", "") + if theZone:hasProperty("count?") then + theZone.triggerCountFlag = theZone:getStringFromZoneProperty("count?", "") + elseif theZone:hasProperty("clock?") then + theZone.triggerCountFlag = theZone:getStringFromZoneProperty("clock?", "") -- can also use in? for counting. we always use triggerCountFlag - elseif cfxZones.hasProperty(theZone, "in?") then - theZone.triggerCountFlag = cfxZones.getStringFromZoneProperty(theZone, "in?", "") + elseif theZone:hasProperty("in?") then + theZone.triggerCountFlag = theZone:getStringFromZoneProperty("in?", "") end if theZone.triggerCountFlag then @@ -118,40 +106,40 @@ function countDown.createCountDownWithZone(theZone) end -- reset - if cfxZones.hasProperty(theZone, "reset?") then - theZone.resetFlag = cfxZones.getStringFromZoneProperty(theZone, "reset?", "") + if theZone:hasProperty("reset?") then + theZone.resetFlag = theZone:getStringFromZoneProperty("reset?", "") theZone.resetFlagValue = cfxZones.getFlagValue(theZone.resetFlag, theZone) end -- zero! bang - if cfxZones.hasProperty(theZone, "zero!") then - theZone.zeroFlag = cfxZones.getStringFromZoneProperty(theZone, "zero!", "") + if theZone:hasProperty("zero!") then + theZone.zeroFlag = theZone:getStringFromZoneProperty("zero!", "") end - if cfxZones.hasProperty(theZone, "out!") then - theZone.zeroFlag = cfxZones.getStringFromZoneProperty(theZone, "out!", "") + if theZone:hasProperty("out!") then + theZone.zeroFlag = theZone:getStringFromZoneProperty("out!", "") end -- TMinus! bang - if cfxZones.hasProperty(theZone, "tMinus!") then - theZone.tMinusFlag = cfxZones.getStringFromZoneProperty(theZone, "tMinus!", "") + if theZone:hasProperty("tMinus!") then + theZone.tMinusFlag = theZone:getStringFromZoneProperty("tMinus!", "") end -- counterOut val - if cfxZones.hasProperty(theZone, "counterOut!") then - theZone.counterOut = cfxZones.getStringFromZoneProperty(theZone, "counterOut!", "") + if theZone:hasProperty("counterOut#") then + theZone.counterOut = theZone:getStringFromZoneProperty( "counterOut#", "") end -- disableFlag/enableFlag theZone.counterDisabled = false - if cfxZones.hasProperty(theZone, "disableCounter?") then - theZone.disableCounterFlag = cfxZones.getStringFromZoneProperty(theZone, "disableCounter?", "") - theZone.disableCounterFlagVal = cfxZones.getFlagValue(theZone.disableCounterFlag, theZone) + if theZone:hasProperty("disableCounter?") then + theZone.disableCounterFlag = theZone:getStringFromZoneProperty("disableCounter?", "") + theZone.disableCounterFlagVal = theZone:getFlagValue(theZone.disableCounterFlag) end - if cfxZones.hasProperty(theZone, "enableCounter?") then - theZone.enableCounterFlag = cfxZones.getStringFromZoneProperty(theZone, "enableCounter?", "") - theZone.enableCounterFlagVal = cfxZones.getFlagValue(theZone.enableCounterFlag, theZone) + if theZone:hasProperty("enableCounter?") then + theZone.enableCounterFlag = theZone:getStringFromZoneProperty("enableCounter?", "") + theZone.enableCounterFlagVal = theZone:getFlagValue(theZone.enableCounterFlag) end end @@ -170,7 +158,7 @@ function countDown.reset(theZone) cfxZones.setFlagValue(theZone.counterOut, val, theZone) end -- read and ignore any pulling of the clock flag - local ignore = cfxZones.testZoneFlag(theZone, theZone.triggerCountFlag, theZone.ctdwnTriggerMethod, "lastCountTriggerValue") + local ignore = theZone:testZoneFlag(theZone.triggerCountFlag, theZone.ctdwnTriggerMethod, "lastCountTriggerValue") -- simply updates lastTriggerValue to current clock value end @@ -186,7 +174,7 @@ function countDown.isTriggered(theZone) local looping = false if theZone.counterOut then - cfxZones.setFlagValue(theZone.counterOut, val, theZone) + theZone:setFlagValue(theZone.counterOut, val) end if val > 0 then @@ -196,7 +184,7 @@ function countDown.isTriggered(theZone) if countDown.verbose or theZone.verbose then trigger.action.outText("+++cntD: <" .. theZone.name .. "> TMINUTS on flag <" .. theZone.tMinusFlag .. ">", 30) end - cfxZones.pollFlag(theZone.tMinusFlag, theZone.ctdwnMethod, theZone) + theZone:pollFlag(theZone.tMinusFlag, theZone.ctdwnMethod) end elseif val == 0 then @@ -206,7 +194,7 @@ function countDown.isTriggered(theZone) if countDown.verbose or theZone.verbose then trigger.action.outText("+++cntD: ZERO <" .. theZone.name .. "> on flag <" .. theZone.zeroFlag .. ">", 30) end - cfxZones.pollFlag(theZone.zeroFlag, theZone.ctdwnMethod, theZone) + theZone:pollFlag(theZone.zeroFlag, theZone.ctdwnMethod) end if theZone.loop then @@ -225,7 +213,7 @@ function countDown.isTriggered(theZone) if countDown.verbose or theZone.verbose then trigger.action.outText("+++cntD: Below Zero", 30) end - cfxZones.pollFlag(theZone.zeroFlag, theZone.ctdwnMethod, theZone) + theZone:pollFlag(theZone.zeroFlag, theZone.ctdwnMethod) end end @@ -243,7 +231,7 @@ function countDown.update() for idx, aZone in pairs(countDown.counters) do if aZone.resetFlag then - if cfxZones.testZoneFlag(aZone, aZone.resetFlag, aZone.ctdwnTriggerMethod, "resetFlagValue") then + if aZone:testZoneFlag(aZone.resetFlag, aZone.ctdwnTriggerMethod, "resetFlagValue") then -- reset pulled, reset the timer to start condition countDown.reset(aZone) end @@ -252,7 +240,7 @@ function countDown.update() -- make sure to re-start before reading time limit -- if reset, lastTriggerValue is updated and will not trigger if (not aZone.counterDisabled) and - cfxZones.testZoneFlag(aZone, aZone.triggerCountFlag, aZone.ctdwnTriggerMethod, "lastCountTriggerValue") + aZone:testZoneFlag(aZone.triggerCountFlag, aZone.ctdwnTriggerMethod, "lastCountTriggerValue") then if countDown.verbose then trigger.action.outText("+++cntD: triggered on in?", 30) @@ -260,14 +248,14 @@ function countDown.update() countDown.isTriggered(aZone) end - if cfxZones.testZoneFlag(aZone, aZone.disableCounterFlag, aZone.ctdwnTriggerMethod, "disableCounterFlagVal") then + if aZone:testZoneFlag(aZone.disableCounterFlag, aZone.ctdwnTriggerMethod, "disableCounterFlagVal") then if countDown.verbose then trigger.action.outText("+++cntD: disabling counter " .. aZone.name, 30) end aZone.counterDisabled = true end - if cfxZones.testZoneFlag(aZone, aZone.enableCounterFlag, aZone.ctdwnTriggerMethod, "enableCounterFlagVal") then + if aZone:testZoneFlag(aZone.enableCounterFlag, aZone.ctdwnTriggerMethod, "enableCounterFlagVal") then if countDown.verbose then trigger.action.outText("+++cntD: ENabling counter " .. aZone.name, 30) end @@ -282,16 +270,13 @@ end function countDown.readConfigZone() local theZone = cfxZones.getZoneByName("countDownConfig") if not theZone then - if countDown.verbose then - trigger.action.outText("+++cntD: NO config zone!", 30) - end - return + theZone = cfxZones.createSimpleZone("countDownConfig") end - countDown.ups = cfxZones.getNumberFromZoneProperty(theZone, "ups", 1) + countDown.ups = theZone:getNumberFromZoneProperty("ups", 1) if countDown.ups < 0.001 then countDown.ups = 0.001 end - countDown.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + countDown.verbose = theZone.verbose if countDown.verbose then trigger.action.outText("+++cntD: read config", 30) diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index c7e27d6..c2b3c4f 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,183 +1,13 @@ dcsCommon = {} -dcsCommon.version = "2.9.8" ---[[-- VERSION HISTORY - 2.2.6 - compassPositionOfARelativeToB - - clockPositionOfARelativeToB - 2.2.7 - isTroopCarrier - - distFlat - 2.2.8 - fixed event2text - 2.2.9 - getUnitAGL - - getUnitAlt - - getUnitSpeed - - getUnitHeading - - getUnitHeadingDegrees - - mag - - clockPositionOfARelativeToB with own heading - 2.3.0 - unitIsInfantry - 2.3.1 - bool2YesNo - - bool2Text - 2.3.2 - getGroupAvgSpeed - - getGroupMaxSpeed - 2.3.3 - getSizeOfTable - 2.3.4 - isSceneryObject - coalition2county - 2.3.5 - smallRandom - pickRandom uses smallRandom - airfield handling, parking - flight waypoint handling - landing waypoint creation - take-off waypoint creation - 2.3.6 - createOverheadAirdromeRoutPintData(aerodrome) - 2.3.7 - coalition2county - warning when creating UN - 2.3.8 - improved headingOfBInDegrees, new getClockDirection - 2.3.9 - getClosingVelocity - - dot product - - magSquare - - vMag - 2.4.0 - libCheck - 2.4.1 - grid/square/rect formation - - arrangeGroupInNColumns formation - - 2Columns formation deep and wide formation - 2.4.2 - getAirbasesInRangeOfPoint - 2.4.3 - lerp - 2.4.4 - getClosestAirbaseTo - - fixed bug in containsString when strings equal - 2.4.5 - added cargo and mass options to createStaticObjectData - 2.4.6 - fixed randompercent - 2.4.7 - smokeColor2Num(smokeColor) - 2.4.8 - linkStaticDataToUnit() - 2.4.9 - trim functions - - createGroundUnitData uses trim function to remove leading/trailing blanks - so now we can use blanks after comma to separate types - - dcsCommon.trimArray( - - createStaticObjectData uses trim for type - - getEnemyCoalitionFor understands strings, still returns number - - coalition2county also understands 'red' and 'blue' - 2.5.0 - "Line" formation with one unit places unit at center - 2.5.1 - vNorm(a) - 2.5.1 - added SA-18 Igla manpad to unitIsInfantry() - 2.5.2 - added copyArray method - - corrected heading in createStaticObjectData - 2.5.3 - corrected rotateGroupData bug for cz - - removed forced error in failed pickRandom - 2.5.4 - rotateUnitData() - - randomBetween() - 2.5.5 - stringStartsWithDigit() - - stringStartsWithLetter() - - stringIsPositiveNumber() - 2.5.6 - corrected stringEndsWith() bug with str - 2.5.7 - point2text(p) - 2.5.8 - string2GroupCat() - 2.5.9 - string2ObjectCat() - 2.6.0 - unified uuid, removed uuIdent - 2.6.1 - removed bug in rotateUnitData: cy --> cz param passing - 2.6.2 - new combineTables() - 2.6.3 - new tacan2freq() - 2.6.4 - new processHMS() - 2.6.5 - new bearing2compass() - - new bearingdegrees2compass() - - new latLon2Text() - based on mist - 2.6.6 - new nowString() - - new str2num() - - new stringRemainsStartingWith() - - new stripLF() - - new removeBlanks() - 2.6.7 - new menu2text() - 2.6.8 - new getMissionName() - - new flagArrayFromString() - 2.6.9 - new getSceneryObjectsInZone() - - new getSceneryObjectInZoneByName() - 2.7.0 - new synchGroupData() - clone, topClone and copyArray now all nil-trap - 2.7.1 - new isPlayerUnit() -- moved from cfxPlayer - new getAllExistingPlayerUnitsRaw - from cfxPlayer - new typeIsInfantry() - 2.7.2 - new rangeArrayFromString() - fixed leading blank bug in flagArrayFromString - new incFlag() - new decFlag() - nil trap in stringStartsWith() - new getClosestFreeSlotForCatInAirbaseTo() - 2.7.3 - new string2Array() - - additional guard for isPlayerUnit - 2.7.4 - new array2string() - 2.7.5 - new bitAND32() - - new LSR() - - new num2bin() - 2.7.6 - new getObjectsForCatAtPointWithRadius() - 2.7.7 - clone() has new stripMeta option. pass true to remove all meta tables - - dumpVar2Str detects meta tables - - rotateGroupData kills unit's psi value if it existed since it messes with heading - - rotateGroupData - changes psi to -heading if it exists rather than nilling - 2.7.8 - new getGeneralDirection() - - new getNauticalDirection() - - more robust guards for getUnitSpeed - 2.7.9 - new bool2Num(theBool) - - new aspectByDirection() - - createGroundGroupWithUnits corrected spelling of minDist, crashed scattered formation - - randomPointInCircle fixed erroneous local for x, z - - "scattered" formation repaired - 2.7.10- semaphore groundwork - 2.8.0 - new collectMissionIDs at start-up - - new getUnitNameByID - - new getGroupNameByID - - bool2YesNo alsco can return NIL - - new getUnitStartPosByID - 2.8.1 - arrayContainsString: type checking for theArray and warning - - processStringWildcards() - - new wildArrayContainsString() - - fix for stringStartsWith oddity with aircraft types - 2.8.2 - better fixes for string.find() in stringStartsWith and containsString - - dcsCommon.isTroopCarrier(theUnit, carriers) new carriers optional param - - better guards for getUnitAlt and getUnitAGL - - new newPointAtDegreesRange() - - new newPointAtAngleRange() - - new isTroopCarrierType() - - stringStartsWith now supports case insensitive match - - isTroopCarrier() supports 'any' and 'all' - - made getEnemyCoalitionFor() more resilient - - fix to smallRandom for negative numbers - - isTroopCarrierType uses wildArrayContainsString - 2.8.3 - small optimizations in bearingFromAtoB() - - new whichSideOfMine() - 2.8.4 - new rotatePointAroundOriginRad() - - new rotatePointAroundPointDeg() - - new rotatePointAroundPointRad() - - getClosestAirbaseTo() now supports passing list of air bases - 2.8.5 - better guard in getGroupUnit() - 2.8.6 - phonetic helpers - new spellString() - 2.8.7 - new flareColor2Num() - - new flareColor2Text() - - new iteratePlayers() - 2.8.8 - new hexString2RGBA() - - new playerName2Coalition() - - new coalition2Text() - 2.8.9 - vAdd supports xy and xyz - - vSub supports xy and xyz - - vMultScalar supports xy and xyz -2.8.10 - tacan2freq now integrated with module (blush) - - array2string cosmetic default - - vMultScalar corrected bug in accessing b.z - - new randomLetter() - - new getPlayerUnit() - - new getMapName() - - new getMagDeclForPoint() -2.9.0 - createPoint() moved from cfxZones - - copyPoint() moved from cfxZones - - numberArrayFromString() moved from cfxZones -2.9.1 - new createSimpleRoutePointData() - - createOverheadAirdromeRoutPintData corrected and legacy support added - - new bearingFromAtoBusingXY() - - corrected verbosity for bearingFromAtoB - - new getCountriesForCoalition() -2.9.2 - updated event2text -2.9.3 - getAirbasesWhoseNameContains now supports category tables for filtering -2.9.4 - new bearing2degrees() -2.9.5 - distanceOfPointPToLineXZ(p, p1, p2) -2.9.6 - new addToTableIfNew() -2.9.7 - createSimpleRoutePointData also accepts speed -2.9.8 - isSceneryObject(theUnit) optimization, DCS 2.9 safe +dcsCommon.version = "3.0.0" +--[[-- VERSION HISTORY +3.0.0 - removed bad bug in stringStartsWith, only relevant if caseSensitive is false + - point2text new intsOnly option + - arrangeGroupDataIntoFormation minDist harden + - cleanup + - new pointInDirectionOfPointXYY() + - createGroundGroupWithUnits now supports liveries + - new getAllExistingPlayersAndUnits() --]]-- -- dcsCommon is a library of common lua functions @@ -511,7 +341,6 @@ dcsCommon.version = "2.9.8" -- a table containing categories, e.g. {0, 2} = airfields and ships but not farps -- if no name given or aName = "*", then all bases are returned prior to filtering function dcsCommon.getAirbasesWhoseNameContains(aName, filterCat, filterCoalition) - --trigger.action.outText("getAB(name): enter with " .. aName, 30) if not aName then aName = "*" end local allYourBase = world.getAirbases() -- get em all local areBelongToUs = {} @@ -520,9 +349,6 @@ dcsCommon.version = "2.9.8" local airBaseName = aBase:getName() -- get display name if aName == "*" or dcsCommon.containsString(airBaseName, aName) then -- containsString is case insesitive unless told otherwise - --if aName ~= "*" then - -- trigger.action.outText("getAB(name): matched " .. airBaseName, 30) - --end local doAdd = true if filterCat then local aCat = dcsCommon.getAirbaseCat(aBase) @@ -628,13 +454,11 @@ dcsCommon.version = "2.9.8" for idx, aSlot in pairs(reallyFree) do local sp = {x = aSlot.vTerminalPos.x, y = 0, z = aSlot.vTerminalPos.z} local currDist = dcsCommon.distFlat(p, sp) - --trigger.action.outText("slot <" .. aSlot.Term_Index .. "> has dist " .. math.floor(currDist) .. " and _0 of <" .. aSlot.Term_Index_0 .. ">", 30) if currDist < closestDist then closestSlot = aSlot closestDist = currDist end end - --trigger.action.outText("slot <" .. closestSlot.Term_Index .. "> has closest dist <" .. math.floor(closestDist) .. ">", 30) return closestSlot end @@ -888,12 +712,9 @@ dcsCommon.version = "2.9.8" if not A then return "***error:A***" end if not B then return "***error:B***" end if not headingOfBInDegrees then headingOfBInDegrees = 0 end - - local bearing = dcsCommon.bearingInDegreesFromAtoB(B, A) -- returns 0..360 --- trigger.action.outText("+++comm: oclock - bearing = " .. bearing .. " and inHeading = " .. headingOfBInDegrees, 30) + local bearing = dcsCommon.bearingInDegreesFromAtoB(B, A) -- returns 0..360 bearing = bearing - headingOfBInDegrees return dcsCommon.getClockDirection(bearing) - end -- given a heading, return clock with 0 being 12, 180 being 6 etc. @@ -1079,7 +900,6 @@ dcsCommon.version = "2.9.8" end -- if we get here, there was no live unit - --trigger.action.outText("+++cmn: A group has no live units. returning nil", 10) return nil end @@ -1107,7 +927,6 @@ dcsCommon.version = "2.9.8" end -- if we get here, there was no live unit - --trigger.action.outText("+++cmn A group has no live units. returning nil", 10) return nil end @@ -1233,7 +1052,6 @@ dcsCommon.version = "2.9.8" -- when filtering occurs in pre, an alternative 'rejected' handler can be called function dcsCommon.addEventHandler(f, pre, post, rejected) -- returns ID local handler = {} -- build a wrapper and connect the onEvent - --dcsCommon.cbID = dcsCommon.cbID + 1 -- increment unique count handler.id = dcsCommon.uuid("eventHandler") handler.f = f -- the callback itself if (rejected) then handler.rejected = rejected end @@ -1245,7 +1063,6 @@ dcsCommon.version = "2.9.8" function handler:onEvent(event) if not self.pre(event) then if dcsCommon.verbose then --- trigger.action.outText("event " .. event.id .. " discarded by pre-processor", 10) end if (self.rejected) then self.rejected(event) end return @@ -1428,8 +1245,8 @@ dcsCommon.version = "2.9.8" rp.action = "Turning Point" rp.type = "Turning Point" if action then rp.action = action; rp.type = action end -- warning: may not be correct, need to verify later - rp.alt = altitudeInFeet * 0.3048 - rp.speed = knots * 0.514444 -- we use + rp.alt = altitudeInFeet * 0.3048 -- in m + rp.speed = knots * 0.514444 -- we use m/s rp.alt_type = "BARO" if (altType) then rp.alt_type = altType end return rp @@ -1485,7 +1302,7 @@ dcsCommon.version = "2.9.8" rp.action = "From Parking Area" rp.type = "TakeOffParking" - rp.speed = 100; -- in m/s? If so, that's 360 km/h + rp.speed = 100 -- that's 360 km/h rp.alt_type = "BARO" return rp end @@ -1673,7 +1490,6 @@ dcsCommon.version = "2.9.8" -- now do the formation stuff -- make sure that they keep minimum distance --- trigger.action.outText("dcsCommon - processing formation " .. formation .. " with radius = " .. radius, 30) if formation == "LINE_V" then -- top to bottom in zone (heding 0). -- will run through x-coordinate -- use entire radius top to bottom @@ -1682,7 +1498,6 @@ dcsCommon.version = "2.9.8" for i=1, num do local u = theNewGroup.units[i] --- trigger.action.outText("formation unit " .. u.name .. " currX = " .. currX, 30) u.x = currX currX = currX + increment end @@ -1698,7 +1513,6 @@ dcsCommon.version = "2.9.8" local increment = radius * 2/(num - 1) -- MUST NOT TRY WITH 1 UNIT! for i=1, num do local u = theNewGroup.units[i] --- trigger.action.outText("formation unit " .. u.name .. " currX = " .. currY, 30) u.y = currY currY = currY + increment end @@ -1713,7 +1527,6 @@ dcsCommon.version = "2.9.8" local incrementX = radius * 2/(num - 1) -- MUST NOT TRY WITH 1 UNIT! for i=1, num do local u = theNewGroup.units[i] --- trigger.action.outText("formation unit " .. u.name .. " currX = " .. currX .. " currY = " .. currY, 30) u.x = currX u.y = currY -- calc coords for NEXT iteration @@ -1731,6 +1544,7 @@ dcsCommon.version = "2.9.8" elseif formation == "SCATTERED" or formation == "RANDOM" then -- use randomPointInCircle and tehn iterate over all vehicles for mindelta processedUnits = {} + if not minDist then minDist = 10 end for i=1, num do local emergencyBreak = 1 -- prevent endless loop local lowDist = 10000 @@ -1760,7 +1574,6 @@ dcsCommon.version = "2.9.8" elseif dcsCommon.stringStartsWith(formation, "CIRCLE") then -- units are arranged on perimeter of circle defined by radius --- trigger.action.outText("formation circle detected", 30) local currAngle = 0 local angleInc = 2 * 3.14157 / num -- increase per spoke for i=1, num do @@ -1786,40 +1599,7 @@ dcsCommon.version = "2.9.8" -- calculate w local w = math.floor(num^(0.5) + 0.5) dcsCommon.arrangeGroupInNColumns(theNewGroup, w, radius) - --[[-- - local h = math.floor(num / w) - --trigger.action.outText("AdcsC: num=" .. num .. " w=" .. w .. "h=" .. h .. " -- num%w=" .. num%w, 30) - if (num % w) > 0 then - h = h + 1 - end - --trigger.action.outText("BdcsC: num=" .. num .. " w=" .. w .. "h=" .. h, 30) - - -- now w * h always >= num and num items fir in that grid - -- w is width, h is height, of course :) - -- now calculat xInc and yInc - local i = 1 - local xInc = 0 - if w > 1 then xInc = 2 * radius / (w-1) end - local yInc = 0 - if h > 1 then yInc = 2 * radius / (h-1) end - local currY = radius - if h < 2 then currY = 0 end -- special:_ place in Y middle if only one row) - while h > 0 do - local currX = radius - local wCnt = w - while wCnt > 0 and (i <= num) do - local u = theNewGroup.units[i] -- get unit - u.x = currX - u.y = currY - currX = currX - xInc - wCnt = wCnt - 1 - i = i + 1 - end - currY = currY - yInc - h = h - 1 - end - --]]-- elseif formation == "2DEEP" or formation == "2COLS" then if num < 2 then return end -- arrange units in an 2 x h grid @@ -1896,11 +1676,14 @@ dcsCommon.version = "2.9.8" end - function dcsCommon.createGroundGroupWithUnits(name, theUnitTypes, radius, minDist, formation, innerRadius) + function dcsCommon.createGroundGroupWithUnits(name, theUnitTypes, radius, minDist, formation, innerRadius, liveries) + -- liveries is indexed by typeName and provides alternate livery names + -- from default. if not minDist then minDist = 4 end -- meters if not formation then formation = "line" end if not radius then radius = 30 end -- meters if not innerRadius then innerRadius = 0 end + if not liveries then liveries = {} end formation = formation:upper() -- theUnitTypes can be either a single string or a table of strings -- see here for TypeName https://github.com/mrSkortch/DCS-miscScripts/tree/master/ObjectDB @@ -1919,14 +1702,9 @@ dcsCommon.version = "2.9.8" local theNewGroup = dcsCommon.createEmptyGroundGroupData(name) -- now add a single unit or multiple units - if type(theUnitTypes) ~= "table" then --- trigger.action.outText("dcsCommon - i am here", 30) --- trigger.action.outText("dcsCommon - name " .. name, 30) --- trigger.action.outText("dcsCommon - unit type " .. theUnitTypes, 30) - + if type(theUnitTypes) ~= "table" then local aUnit = {} aUnit = dcsCommon.createGroundUnitData(name .. "-1", theUnitTypes, false) --- trigger.action.outText("dcsCommon - unit name retval " .. aUnit.name, 30) dcsCommon.addUnitToGroupData(aUnit, theNewGroup, 0, 0) -- create with data at location (0,0) return theNewGroup end @@ -1937,6 +1715,10 @@ dcsCommon.version = "2.9.8" for key, theType in pairs(theUnitTypes) do -- trigger.action.outText("+++dcsC: creating unit " .. name .. "-" .. num .. ": " .. theType, 30) local aUnit = dcsCommon.createGroundUnitData(name .. "-"..num, theType, false) + local theLivery = liveries[theType] + if theLivery then + aUnit.livery_id = theLivery + end dcsCommon.addUnitToGroupData(aUnit, theNewGroup, 0, 0) num = num + 1 end @@ -1991,15 +1773,23 @@ dcsCommon.version = "2.9.8" elseif cat == Group.Category.GROUND then -- we got all we need else - -- trigger.action.outText("dcsCommon - unknown category: " .. cat, 30) - -- return nil - -- we also got all we need + end end end; + function dcsCommon.pointInDirectionOfPointXYY(dir, dist, p) -- dir in rad, p in XYZ returns XYY + local fx = math.cos(dir) + local fy = math.sin(dir) + local p2 = {} + p2.x = p.x + dist * fx + p2.y = p.z + dist * fy + p2.z = p2.y -- make p2 XYY vec2/3 upcast + return p2 + end + function dcsCommon.rotatePointAroundOriginRad(inX, inY, angle) -- angle in degrees local c = math.cos(angle) local s = math.sin(angle) @@ -2046,7 +1836,6 @@ dcsCommon.version = "2.9.8" if not cx then cx = 0 end if not cz then cz = 0 end local cy = cz - --trigger.action.outText("+++dcsC:rotGrp cy,cy = "..cx .. "," .. cy, 30) local rads = degrees * 3.14152 / 180 do @@ -2066,7 +1855,6 @@ dcsCommon.version = "2.9.8" if not cx then cx = 0 end if not cz then cz = 0 end local cy = cz - --trigger.action.outText("+++dcsC:rotGrp cy,cy = "..cx .. "," .. cy, 30) local rads = degrees * 3.14152 / 180 -- turns all units in group around the group's center by degrees. @@ -2080,9 +1868,6 @@ dcsCommon.version = "2.9.8" -- may also want to increase heading by degrees theUnit.heading = theUnit.heading + rads - -- now kill psi if it existed before - -- theUnit.psi = nil - -- better code: psi is always -heading. Nobody knows what psi is, though if theUnit.psi then theUnit.psi = -theUnit.heading end @@ -2247,33 +2032,27 @@ end end if not caseSensitive then theString = string.upper(theString) end - --trigger.action.outText("wildACS: theString = <" .. theString .. ">, theArray contains <" .. #theArray .. "> elements", 30) local wildIn = dcsCommon.stringEndsWith(theString, "*") if wildIn then dcsCommon.removeEnding(theString, "*") end for idx, theElement in pairs(theArray) do -- i = 1, #theArray do - --local theElement = theArray[i] - --trigger.action.outText("test e <" .. theElement .. "> against s <" .. theString .. ">", 30) if not caseSensitive then theElement = string.upper(theElement) end local wildEle = dcsCommon.stringEndsWith(theElement, "*") if wildEle then theElement = dcsCommon.removeEnding(theElement, "*") end - --trigger.action.outText("matching s=<" .. theString .. "> with e=<" .. theElement .. ">", 30) + if wildEle and wildIn then -- both end on wildcards, partial match for both if dcsCommon.stringStartsWith(theElement, theString) then return true end if dcsCommon.stringStartsWith(theString, theElement) then return true end - --trigger.action.outText("match e* with s* failed.", 30) elseif wildEle then -- Element is a wildcard, partial match if dcsCommon.stringStartsWith(theString, theElement) then return true end - --trigger.action.outText("startswith - match e* <" .. theElement .. "> with s <" .. theString .. "> failed.", 30) + elseif wildIn then -- theString is a wildcard. partial match if dcsCommon.stringStartsWith(theElement, theString) then return true end - --trigger.action.outText("match e with s* failed.", 30) else -- standard: no wildcards, full match if theElement == theString then return true end - --trigger.action.outText("match e with s (straight) failed.", 30) end end @@ -2412,7 +2191,7 @@ end if caseInsensitive then theString = string.upper(theString) - thePrefix = string.upper(theString) + thePrefix = string.upper(thePrefix) end -- superseded: string.find (s, pattern [, init [, plain]]) solves the problem local i, j = string.find(theString, thePrefix, 1, true) @@ -2467,12 +2246,19 @@ end return 0 end - function dcsCommon.point2text(p) + function dcsCommon.point2text(p, intsOnly) + if not intsOnly then intsOnly = false end if not p then return "" end local t = "[x=" - if p.x then t = t .. p.x .. ", " else t = t .. ", " end - if p.y then t = t .. "y=" .. p.y .. ", " else t = t .. "y=, " end - if p.z then t = t .. "z=" .. p.z .. "]" else t = t .. "z=]" end + if intsOnly then + if p.x then t = t .. math.floor(p.x) .. ", " else t = t .. ", " end + if p.y then t = t .. "y=" .. math.floor(p.y) .. ", " else t = t .. "y=, " end + if p.z then t = t .. "z=" .. math.floor(p.z) .. "]" else t = t .. "z=]" end + else + if p.x then t = t .. p.x .. ", " else t = t .. ", " end + if p.y then t = t .. "y=" .. p.y .. ", " else t = t .. "y=, " end + if p.z then t = t .. "z=" .. p.z .. "]" else t = t .. "z=]" end + end return t end @@ -2605,7 +2391,6 @@ end if not inrecursion then -- output a marker to find in the log / screen trigger.action.outText("=== dcsCommon vardump end", 30) - --env.info("=== dcsCommon vardump end") end end @@ -2636,7 +2421,9 @@ end "BDA", "AI Abort Mission", "DayNight", "Flight Time", -- 40 "Pilot Suicide", "player cap airfield", "emergency landing", "unit create task", -- 44 "unit delete task", "Simulation start", "weapon rearm", "weapon drop", -- 48 - "unit task timeout", "unit task stage", + "unit task timeout", "unit task stage", -- 50 + "subtask score", "extra score", "mission restart", "winner", + "postponed takeoff", "postponed land", -- 56 "max"} if id > #events then return "Unknown (ID=" .. id .. ")" end return events[id] @@ -2926,6 +2713,21 @@ function dcsCommon.getAllExistingPlayerUnitsRaw() return apu end +function dcsCommon.getAllExistingPlayersAndUnits() -- units indexed by player name +-- designed to replace cases for cfxPlayer.getAllPlayer invocations + local apu = {} + 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() then + local pName = theUnit:getPlayerName() + apu[pName] = theUnit + end + end + end + return apu +end + function dcsCommon.getUnitAlt(theUnit) if not theUnit then return 0 end if not Unit.isExist(theUnit) then return 0 end -- safer @@ -3028,16 +2830,6 @@ function dcsCommon.unitIsInfantry(theUnit) if not theUnit then return false end if not theUnit:isExist() then return end local theType = theUnit:getTypeName() ---[[-- - local isInfantry = - dcsCommon.containsString(theType, "infantry", false) or - dcsCommon.containsString(theType, "paratrooper", false) or - dcsCommon.containsString(theType, "stinger", false) or - dcsCommon.containsString(theType, "manpad", false) or - dcsCommon.containsString(theType, "soldier", false) or - dcsCommon.containsString(theType, "SA-18 Igla", false) - return isInfantry ---]]-- return dcsCommon.typeIsInfantry(theType) end diff --git a/modules/delayFlags.lua b/modules/delayFlags.lua index ddd944b..9732ee1 100644 --- a/modules/delayFlags.lua +++ b/modules/delayFlags.lua @@ -1,5 +1,5 @@ delayFlag = {} -delayFlag.version = "1.4.0" +delayFlag.version = "2.0.0" delayFlag.verbose = false delayFlag.requiredLibs = { "dcsCommon", -- always @@ -11,35 +11,12 @@ delayFlag.flags = {} delay flags - simple flag switch & delay, allows for randomize and dead man switching - Copyright (c) 2022 by Christian Franz and cf/x AG + Copyright (c) 2022-2024 by Christian Franz and cf/x AG Version History - 1.0.0 - Initial Version - 1.0.1 - message attribute - 1.0.2 - slight spelling correction - - using cfxZones for polling - - removed pollFlag - 1.0.3 - bug fix for config zone name - - removed message attribute, moved to own module - - triggerFlag --> triggerDelayFlag - 1.0.4 - startDelay - 1.1.0 - DML flag upgrade - - removed onStart. use local raiseFlag instead - - delayDone! synonym - - pauseDelay? - - unpauseDelay? - 1.2.0 - Watchflags - 1.2.1 - method goes to dlyMethod - - delay done is correctly inited - 1.2.2 - delayMethod defaults to inc - - zone-local verbosity - - code clean-up - 1.2.3 - pauseDelay - - continueDelay - - delayLeft - 1.3.0 - persistence 1.4.0 - dmlZones - delayLeft# + 2.0.0 - clean-up --]]-- @@ -317,7 +294,7 @@ function delayFlag.readConfigZone() theZone = cfxZones.createSimpleZone("delayFlagsConfig") end - delayFlag.verbose = theZone.verbose -- cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + delayFlag.verbose = theZone.verbose if delayFlag.verbose then trigger.action.outText("+++dlyF: read config", 30) diff --git a/modules/factoryZone.lua b/modules/factoryZone.lua index c6762e2..95a2170 100644 --- a/modules/factoryZone.lua +++ b/modules/factoryZone.lua @@ -1,5 +1,5 @@ factoryZone = {} -factoryZone.version = "2.0.0" +factoryZone.version = "3.0.0" factoryZone.verbose = false factoryZone.name = "factoryZone" @@ -9,6 +9,8 @@ factoryZone.name = "factoryZone" - "production" and "defenders" simplification - now optional specification for red/blue - use maxRadius from zone for spawning to support quad zones +3.0.0 - support for liveries via "factoryLiveries" zone + - OOP dmlZones --]]-- factoryZone.requiredLibs = { @@ -20,6 +22,7 @@ factoryZone.requiredLibs = { } factoryZone.zones = {} -- my factory zones +factoryZone.liveries = {} -- indexed by type name factoryZone.ups = 1 factoryZone.initialized = false factoryZone.defendingTime = 100 -- seconds until new defenders are produced @@ -28,7 +31,7 @@ factoryZone.shockTime = 200 -- 'shocked' period of inactivity factoryZone.repairTime = 200 -- time until we raplace one lost unit, also repairs all other units to 100% -- persistence: all attackers we ever sent out. --- is regularly verified and cut to size +-- is regularly verified and cut to size via GC factoryZone.spawnedAttackers = {} -- factoryZone is a module that manages production of units @@ -38,24 +41,6 @@ factoryZone.spawnedAttackers = {} -- -- *** EXTENTDS ZONES *** -- --- zone attributes when owned --- owner: coalition that owns the zone. Managed externally --- status: FSM for spawning --- defendersRED/BLUE - coma separated type string for the group to spawn on defense cycle completion --- attackersRED/BLUE - as above for attack cycle. --- timeStamp - time when zone switched into current state --- spawnRadius - overrides zone's radius when placing defenders. can be use to place defenders inside or outside zone itself --- formation - defender's formation --- attackFormation - attackers formation --- attackRadius - radius of circle in which attackers are spawned. informs formation --- attackDelta - polar coord: r from zone center where attackers are spawned --- attackPhi - polar degrees where attackers are to be spawned --- paused - will not spawn. default is false - --- --- M I S C --- - function factoryZone.getFactoryZoneByName(zName) for zKey, theZone in pairs (factoryZone.zones) do @@ -65,59 +50,58 @@ function factoryZone.getFactoryZoneByName(zName) end function factoryZone.addFactoryZone(aZone) - --aZone.worksFor = cfxZones.getCoalitionFromZoneProperty(aZone, "factory", 0) -- currently unused, have RED/BLUE separate types aZone.state = "init" aZone.timeStamp = timer.getTime() -- set up production default - local factory = cfxZones.getStringFromZoneProperty(aZone, "factory", "none") + local factory = aZone:getStringFromZoneProperty("factory", "none") - local production = cfxZones.getStringFromZoneProperty(aZone, "production", factory) + local production = aZone:getStringFromZoneProperty("production", factory) - local defenders = cfxZones.getStringFromZoneProperty(aZone, "defenders", factory) + local defenders = aZone:getStringFromZoneProperty("defenders", factory) - if cfxZones.hasProperty(aZone, "attackersRED") then + if aZone:hasProperty("attackersRED") then -- legacy support - aZone.attackersRED = cfxZones.getStringFromZoneProperty(aZone, "attackersRED", production) + aZone.attackersRED = aZone:getStringFromZoneProperty( "attackersRED", production) else - aZone.attackersRED = cfxZones.getStringFromZoneProperty(aZone, "productionRED", production) + aZone.attackersRED = aZone:getStringFromZoneProperty( "productionRED", production) end - if cfxZones.hasProperty(aZone, "attackersBLUE") then + if aZone:hasProperty("attackersBLUE") then -- legacy support - aZone.attackersBLUE = cfxZones.getStringFromZoneProperty(aZone, "attackersBLUE", production) + aZone.attackersBLUE = aZone:getStringFromZoneProperty( "attackersBLUE", production) else - aZone.attackersBLUE = cfxZones.getStringFromZoneProperty(aZone, "productionBLUE", production) + aZone.attackersBLUE = aZone:getStringFromZoneProperty( "productionBLUE", production) end -- set up defenders default, or use production / factory - aZone.defendersRED = cfxZones.getStringFromZoneProperty(aZone, "defendersRED", defenders) - aZone.defendersBLUE = cfxZones.getStringFromZoneProperty(aZone, "defendersBLUE", defenders) + aZone.defendersRED = aZone:getStringFromZoneProperty("defendersRED", defenders) + aZone.defendersBLUE = aZone:getStringFromZoneProperty("defendersBLUE", defenders) - aZone.formation = cfxZones.getStringFromZoneProperty(aZone, "formation", "circle_out") - aZone.attackFormation = cfxZones.getStringFromZoneProperty(aZone, "attackFormation", "circle_out") -- cfxZones.getZoneProperty(aZone, "attackFormation") - aZone.spawnRadius = cfxZones.getNumberFromZoneProperty(aZone, "spawnRadius", aZone.maxRadius-5) -- "-5" so they remaininside radius - aZone.attackRadius = cfxZones.getNumberFromZoneProperty(aZone, "attackRadius", aZone.maxRadius) - aZone.attackDelta = cfxZones.getNumberFromZoneProperty(aZone, "attackDelta", 10) -- aZone.radius) - aZone.attackPhi = cfxZones.getNumberFromZoneProperty(aZone, "attackPhi", 0) + aZone.formation = aZone:getStringFromZoneProperty("formation", "circle_out") + aZone.attackFormation = aZone:getStringFromZoneProperty( "attackFormation", "circle_out") -- cfxZones.getZoneProperty(aZone, "attackFormation") + aZone.spawnRadius = aZone:getNumberFromZoneProperty("spawnRadius", aZone.maxRadius-5) -- "-5" so they remaininside radius + aZone.attackRadius = aZone:getNumberFromZoneProperty("attackRadius", aZone.maxRadius) + aZone.attackDelta = aZone:getNumberFromZoneProperty("attackDelta", 10) -- aZone.radius) + aZone.attackPhi = aZone:getNumberFromZoneProperty("attackPhi", 0) - aZone.paused = cfxZones.getBoolFromZoneProperty(aZone, "paused", false) + aZone.paused = aZone:getBoolFromZoneProperty("paused", false) aZone.factoryOwner = aZone.owner -- copy so we can compare next round -- pause? and activate? - if cfxZones.hasProperty(aZone, "pause?") then - aZone.pauseFlag = cfxZones.getStringFromZoneProperty(aZone, "pause?", "none") + if aZone:hasProperty("pause?") then + aZone.pauseFlag = aZone:getStringFromZoneProperty("pause?", "none") aZone.lastPauseValue = trigger.misc.getUserFlag(aZone.pauseFlag) end - if cfxZones.hasProperty(aZone, "activate?") then - aZone.activateFlag = cfxZones.getStringFromZoneProperty(aZone, "activate?", "none") + if aZone:hasProperty("activate?") then + aZone.activateFlag = aZone:getStringFromZoneProperty("activate?", "none") aZone.lastActivateValue = trigger.misc.getUserFlag(aZone.activateFlag) end - aZone.factoryTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "triggerMethod", "change") - if cfxZones.hasProperty(aZone, "factoryTriggerMethod") then - aZone.factoryTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "factoryTriggerMethod", "change") + aZone.factoryTriggerMethod = aZone:getStringFromZoneProperty( "triggerMethod", "change") + if aZone:hasProperty("factoryTriggerMethod") then + aZone.factoryTriggerMethod = aZone:getStringFromZoneProperty( "factoryTriggerMethod", "change") end factoryZone.zones[aZone.name] = aZone @@ -159,11 +143,12 @@ function factoryZone.spawnAttackTroops(theTypes, aZone, aCoalition, aFormation) local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( aCoalition, -- theCountry, - aZone.name .. " (A) " .. dcsCommon.numberUUID(), -- must be unique - spawnZone, - unitTypes, + aZone.name .. " (A) " .. dcsCommon.numberUUID(), + spawnZone, + unitTypes, aFormation, -- outward facing - 0) + 0, + factoryZone.liveries) return theGroup, theData end @@ -184,10 +169,12 @@ function factoryZone.spawnDefensiveTroops(theTypes, aZone, aCoalition, aFormatio local spawnZone = cfxZones.createSimpleZone("spawnZone", aZone.point, aZone.spawnRadius) local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( aCoalition, --theCountry, - aZone.name .. " (D) " .. dcsCommon.numberUUID(), -- must be unique - spawnZone, unitTypes, + aZone.name .. " (D) " .. dcsCommon.numberUUID(), + spawnZone, + unitTypes, aFormation, -- outward facing - 0) + 0, + factoryZone.liveries) return theGroup, theData end @@ -308,7 +295,8 @@ function factoryZone.repairDefenders(aZone) livingTypes, aZone.formation, -- outward facing - 0) + 0, + factoryZone.liveries) aZone.defenders = theGroup aZone.lastDefenders = theGroup:getSize() end @@ -686,15 +674,26 @@ end function factoryZone.readConfigZone(theZone) if not theZone then theZone = cfxZones.createSimpleZone("factoryZoneConfig") end factoryZone.name = "factoryZone" -- just in case, so we can access with cfxZones - factoryZone.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) - factoryZone.defendingTime = cfxZones.getNumberFromZoneProperty(theZone, "defendingTime", 100) - factoryZone.attackingTime = cfxZones.getNumberFromZoneProperty(theZone, "attackingTime", 300) - factoryZone.shockTime = cfxZones.getNumberFromZoneProperty(theZone, "shockTime", 200) - factoryZone.repairTime = cfxZones.getNumberFromZoneProperty(theZone, "repairTime", 200) + factoryZone.verbose = theZone.verbose + factoryZone.defendingTime = theZone:getNumberFromZoneProperty( "defendingTime", 100) + factoryZone.attackingTime = theZone:getNumberFromZoneProperty( "attackingTime", 300) + factoryZone.shockTime = theZone:getNumberFromZoneProperty("shockTime", 200) + factoryZone.repairTime = theZone:getNumberFromZoneProperty( "repairTime", 200) factoryZone.targetZones = "OWNED" end +function factoryZone.readLiveries() + theZone = cfxZones.getZoneByName("factoryLiveries") + if not theZone then return end + factoryZone.liveries = theZone:getAllZoneProperties() + trigger.action.outText("Custom liveries detected. All factories now use:", 30) + for aType, aLivery in pairs (factoryZone.liveries) do + trigger.action.outText(" type <" .. aType .. "> now uses livery <" .. aLivery .. ">", 30) + end +end + + function factoryZone.init() -- check libs if not dcsCommon.libCheck("cfx Factory Zones", @@ -706,12 +705,12 @@ function factoryZone.init() local theZone = cfxZones.getZoneByName("factoryZoneConfig") factoryZone.readConfigZone(theZone) + -- read livery presets for factory production + factoryZone.readLiveries() + -- collect all zones by their 'factory' property -- start the process local pZones = cfxZones.zonesWithProperty("factory") - - -- now add all zones to my zones table, and convert the owner property into - -- a proper attribute for k, aZone in pairs(pZones) do factoryZone.addFactoryZone(aZone) end diff --git a/modules/fireFX.lua b/modules/fireFX.lua index b708860..081d1d9 100644 --- a/modules/fireFX.lua +++ b/modules/fireFX.lua @@ -1,5 +1,5 @@ fireFX = {} -fireFX.version = "1.1.0" +fireFX.version = "2.0.1" fireFX.verbose = false fireFX.ups = 1 fireFX.requiredLibs = { @@ -15,7 +15,7 @@ fireFX.fx = {} 1.1.1 - agl attribute 2.0.0 - dmlZones OOP - rndLoc - + 2.0.1 - fixed rndLoc determination --]]-- function fireFX.addFX(theZone) @@ -92,9 +92,9 @@ function fireFX.createFXWithZone(theZone) end theZone.rndLoc = theZone:getBoolFromZoneProperty("rndLoc", false) - if theZone.max + 1 and (not theZone.rndLoc) then + if theZone.max > 1 and (not theZone.rndLoc) then if theZone.verbose or fireFX.verbose then - trigger.action.outText("+++ffx: more than 1 fires, will set to random loc") + trigger.action.outText("+++ffx: more than 1 fires, will set to random loc", 30) end theZone.rndLoc = true end @@ -113,7 +113,7 @@ function fireFX.startTheFire(theZone) theZone.fireNames = {} local num = cfxZones.randomInRange(theZone.min, theZone.max) for i = 1, num do - local p = cfxZones.getPoint(theZone) + local p = theZone:getPoint() if theZone.rndLoc then p = theZone:randomPointInZone() end diff --git a/modules/cfxGroundTroops.lua b/modules/groundTroops.lua similarity index 100% rename from modules/cfxGroundTroops.lua rename to modules/groundTroops.lua diff --git a/modules/groupTrackers.lua b/modules/groupTrackers.lua index fded185..16e1045 100644 --- a/modules/groupTrackers.lua +++ b/modules/groupTrackers.lua @@ -1,5 +1,5 @@ groupTracker = {} -groupTracker.version = "1.2.1" +groupTracker.version = "2.0.0" groupTracker.verbose = false groupTracker.ups = 1 groupTracker.requiredLibs = { @@ -10,28 +10,7 @@ groupTracker.trackers = {} --[[-- Version History - 1.0.0 - Initial version - 1.1.0 - filtering added - - array support for trackers - - array support for trackers - 1.1.1 - corrected clone zone reference bug - 1.1.2 - corrected naming (removed bang from flags), deprecated old - - more zone-local verbosity - 1.1.3 - spellings - - addGroupToTrackerNamed bug removed accessing tracker - - new removeGroupNamedFromTrackerNamed() - 1.1.4 - destroy? input - - allGone! output - - triggerMethod - - method - - isDead optimiz ation - 1.2.0 - double detection - - numUnits output - - persistence - 1.2.1 - allGone! bug removed - 1.2.2 - new groupTrackedBy() method - - limbo for storing a unit in limbo so it is - - not counted as missing when being transported + 2.0.0 - dmlZones, OOP, clean-up, legacy support --]]-- @@ -259,62 +238,7 @@ function groupTracker.removeGroupNamedFromTrackerNamed(gName, trackerName) end groupTracker.removeGroupNamedFromTracker(gName, theTracker) ---[[-- - local filteredGroups = {} - local foundOne = false - local totalUnits = 0 - if not theTracker.trackedGroups then theTracker.trackedGroups = {} end - for idx, aGroup in pairs(theTracker.trackedGroups) do - if aGroup:getName() == gName then - -- skip and remember - foundOne = true - else - table.insert(filteredGroups, aGroup) - if Group.isExist(aGroup) then - totalUnits = totalUnits + aGroup:getSize() - end - end - end - -- also check limbo - for limboName, limboNum in pairs (theTracker.limbo) do - if gName == limboName then - -- don't count, but remember that it existed - foundOne = true - else - totalUnits = totalUnits + limboNum - end - end - -- remove from limbo - theTracker.limbo[gName] = nil - - if (not foundOne) and (theTracker.verbose or groupTracker.verbose) then - trigger.action.outText("+++gTrk: Removal Request Note: group <" .. gName .. "> wasn't tracked by <" .. trackerName .. ">", 30) - end - - -- remember the new, cleanded set - theTracker.trackedGroups = filteredGroups - - -- update number of tracked units. do it in any case - if theTracker.tNumUnits then - cfxZones.setFlagValue(theTracker.tNumUnits, totalUnits, theTracker) - end - - if foundOne then - if theTracker.verbose or groupTracker.verbose then - trigger.action.outText("+++gTrk: removed group <" .. gName .. "> from tracker <" .. trackerName .. ">", 30) - end - - -- now bang/invoke addGroup! - if theTracker.tRemoveGroup then - cfxZones.pollFlag(theTracker.tRemoveGroup, "inc", theTracker) - end - - -- now set numGroups - if theTracker.tNumGroups then - cfxZones.setFlagValue(theTracker.tNumGroups, dcsCommon.getSizeOfTable(theTracker.limbo) + #theTracker.trackedGroups, theTracker) - end - end - --]]-- + end -- groupTrackedBy - return trackers that track group theGroup @@ -365,62 +289,56 @@ function groupTracker.createTrackerWithZone(theZone) theZone.limbo = {} -- name based, for groups that are tracked -- although technically off the map (helo etc) - theZone.trackerMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") - if cfxZones.hasProperty(theZone, "trackerMethod") then - theZone.trackerMethod = cfxZones.getStringFromZoneProperty(theZone, "trackerMethod", "inc") + theZone.trackerMethod = theZone:getStringFromZoneProperty("method", "inc") + if theZone:hasProperty("trackerMethod") then + theZone.trackerMethod = theZone:getStringFromZoneProperty( "trackerMethod", "inc") end - theZone.trackerTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") + theZone.trackerTriggerMethod = theZone:getStringFromZoneProperty("triggerMethod", "change") - if cfxZones.hasProperty(theZone, "trackerTriggerMethod") then - theZone.trackerTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "trackerTriggerMethod", "change") + if theZone:hasProperty("trackerTriggerMethod") then + theZone.trackerTriggerMethod = theZone:getStringFromZoneProperty("trackerTriggerMethod", "change") end - if cfxZones.hasProperty(theZone, "numGroups") then - theZone.tNumGroups = cfxZones.getStringFromZoneProperty(theZone, "numGroups", "*") - -- we may need to zero this flag - elseif cfxZones.hasProperty(theZone, "numGroups!") then -- DEPRECATED! - theZone.tNumGroups = cfxZones.getStringFromZoneProperty(theZone, "numGroups!", "*") - -- we may need to zero this flag + if theZone:hasProperty("numGroups") then + theZone.tNumGroups = theZone:getStringFromZoneProperty("numGroups", "*") -- legacy support + elseif theZone:hasProperty("numGroups#") then + theZone.tNumGroups = theZone:getStringFromZoneProperty( "numGroups#", "*") end - - if cfxZones.hasProperty(theZone, "numUnits") then - theZone.tNumUnits = cfxZones.getStringFromZoneProperty(theZone, "numUnits", "*") + + if theZone:hasProperty("numUnits") then + theZone.tNumUnits = theZOne:getStringFromZoneProperty("numUnits", "*") -- legacy support + elseif theZone:hasProperty("numUnits#") then + theZone.tNumUnits = theZone:getStringFromZoneProperty("numUnits#", "*") end - if cfxZones.hasProperty(theZone, "addGroup") then - theZone.tAddGroup = cfxZones.getStringFromZoneProperty(theZone, "addGroup", "*") - -- we may need to zero this flag - elseif cfxZones.hasProperty(theZone, "addGroup!") then -- DEPRECATED - theZone.tAddGroup = cfxZones.getStringFromZoneProperty(theZone, "addGroup!", "*") - -- we may need to zero this flag + if theZone:hasProperty("addGroup") then + theZone.tAddGroup = theZone:getStringFromZoneProperty("addGroup", "*") -- legacy support + elseif theZone:hasProperty("addGroup!") then + theZone.tAddGroup = theZone:getStringFromZoneProperty("addGroup!", "*") end - if cfxZones.hasProperty(theZone, "removeGroup") then - theZone.tRemoveGroup = cfxZones.getStringFromZoneProperty(theZone, "removeGroup", "*") - -- we may need to zero this flag - elseif cfxZones.hasProperty(theZone, "removeGroup!") then -- DEPRECATED! - theZone.tRemoveGroup = cfxZones.getStringFromZoneProperty(theZone, "removeGroup!", "*") - -- we may need to zero this flag + if theZone:hasProperty("removeGroup") then + theZone.tRemoveGroup = theZone:getStringFromZoneProperty( "removeGroup", "*") -- legacy support + elseif theZone:hasProperty("removeGroup!") then + theZone.tRemoveGroup = theZone:getStringFromZoneProperty( "removeGroup!", "*") end - - - - if cfxZones.hasProperty(theZone, "groupFilter") then - local filterString = cfxZones.getStringFromZoneProperty(theZone, "groupFilter", "2") -- ground + + if theZone:hasProperty("groupFilter") then + local filterString = theZone:getStringFromZoneProperty( "groupFilter", "2") -- ground theZone.groupFilter = dcsCommon.string2GroupCat(filterString) if groupTracker.verbose or theZone.verbose then trigger.action.outText("+++gTrck: filtering " .. theZone.groupFilter .. " in " .. theZone.name, 30) end end - if cfxZones.hasProperty(theZone, "destroy?") then - theZone.destroyFlag = cfxZones.getStringFromZoneProperty(theZone, "destroy?", "*") + if theZone:hasProperty("destroy?") then + theZone.destroyFlag = theZone:getStringFromZoneProperty(theZone, "destroy?", "*") theZone.lastDestroyValue = cfxZones.getFlagValue(theZone.destroyFlag, theZone) end - if cfxZones.hasProperty(theZone, "allGone!") then - theZone.allGoneFlag = cfxZones.getStringFromZoneProperty(theZone, "allGone!", "") -- note string on number default + if theZone:hasProperty("allGone!") then + theZone.allGoneFlag = theZone:getStringFromZoneProperty("allGone!", "") -- note string on number default end theZone.lastGroupCount = 0 @@ -652,15 +570,12 @@ end function groupTracker.readConfigZone() local theZone = cfxZones.getZoneByName("groupTrackerConfig") if not theZone then - if groupTracker.verbose then - trigger.action.outText("+++gTrk: NO config zone!", 30) - end - return + theZone = cfxZones.createSimpleZone("groupTrackerConfig") end - groupTracker.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + groupTracker.verbose = theZoneverbose - groupTracker.ups = cfxZones.getNumberFromZoneProperty(theZone, "ups", 1) + groupTracker.ups = theZone:getNumberFromZoneProperty("ups", 1) if groupTracker.verbose then trigger.action.outText("+++gTrk: read config", 30) diff --git a/modules/cfxHeloTroops.lua b/modules/heloTroops.lua similarity index 100% rename from modules/cfxHeloTroops.lua rename to modules/heloTroops.lua diff --git a/modules/cfxMapMarkers.lua b/modules/mapMarkers.lua similarity index 100% rename from modules/cfxMapMarkers.lua rename to modules/mapMarkers.lua diff --git a/modules/messenger.lua b/modules/messenger.lua index c1f521a..0761630 100644 --- a/modules/messenger.lua +++ b/modules/messenger.lua @@ -1,5 +1,5 @@ messenger = {} -messenger.version = "2.3.1" +messenger.version = "3.0.0" messenger.verbose = false messenger.requiredLibs = { "dcsCommon", -- always @@ -8,68 +8,9 @@ messenger.requiredLibs = { messenger.messengers = {} --[[-- Version History - 1.0.0 - initial version - 1.0.1 - messageOut? synonym - - spelling types in about - 1.1.0 - DML flag support - - clearScreen option - - inValue? - - message preprocessor - 1.1.1 - firewalled coalition to msgCoalition - - messageOn? - - messageOff? - 1.2.0 - msgTriggerMethod (original Watchflag integration) - 1.2.1 - qoL: = newline, = zone name, = value - 1.3.0 - messenger? saves messageOut? attribute - 1.3.1 - message now can interpret value as time with <:h> <:m> <:s> - 1.3.2 - message interprets as time in HH:MM:SS of current time - - can interpret , , - - zone-local verbosity - 1.3.3 - mute/messageMute option to start messenger in mute - 2.0.0 - re-engineered message wildcards - - corrected dynamic content for time and latlon (classic) - - new timeFormat attribute - - - - - - added - - added imperial - - - - - - - - - - - - - - messageError - - unit - - group - 2.0.1 - config optimization - 2.1.0 - unit only: dynamicUnitProcessing with other units/zones - - bearing to unit/zone - - response mapped by unit's heading - - bearing in clock position to unit/zone - - range to unit/zone - - bearing in left/right/ahead/behind - - bearing in starboard/port/ahead/aft - - added dynamicGroupProcessing to select unit 1 - - responses attribute - - - - response randomized - - respons mapped by unit's heading - - closing speed - - velocity (speed) - - aspect - - fix to messageMute - - - 2.1.1 - cosmetic: only output text if len>0 and not cls - 2.2.0 - - - made dynamic string gen more portable in prep for move to cfxZones - - refactoring wildcard processing: moved to cfxZones - 2.2.1 - when messenger is linked to a unit, it can use the linked - unit as reference point for relative wildcards. Always broadcasts to coalition. Can be used to broadcase 'eye in the sky' type information - - fixed verbosity bug 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? --]]-- function messenger.addMessenger(theZone) @@ -255,11 +196,11 @@ function messenger.createMessengerWithZone(theZone) 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 @@ -505,12 +446,13 @@ function messenger.start() -- process messenger Zones -- old style +--[[-- local attrZones = cfxZones.getZonesWithAttributeNamed("messenger") for k, aZone in pairs(attrZones) do messenger.createMessengerWithZone(aZone) -- process attributes messenger.addMessenger(aZone) -- add to list end - +--]]-- -- new style that saves messageOut? flag by reading flags attrZones = cfxZones.getZonesWithAttributeNamed("messenger?") for k, aZone in pairs(attrZones) do diff --git a/modules/cfxObjectDestructDetector.lua b/modules/objectDestructDetector.lua similarity index 96% rename from modules/cfxObjectDestructDetector.lua rename to modules/objectDestructDetector.lua index ac1619a..89f4a2a 100644 --- a/modules/cfxObjectDestructDetector.lua +++ b/modules/objectDestructDetector.lua @@ -17,6 +17,7 @@ cfxObjectDestructDetector.requiredLibs = { re-wrote object determination to not be affected by ID changes (happens with map updates) fail addZone when name property is missing + 2.0.1 check that the object is within the zone onEvent --]]-- cfxObjectDestructDetector.objectZones = {} @@ -86,6 +87,8 @@ function cfxObjectDestructDetector:onEvent(event) if event.id == world.event.S_EVENT_DEAD then if not event.initiator then return end local theObject = event.initiator + -- check location + local pos = theObject:getPoint() local desc = theObject:getDesc() if not desc then return end local matchMe = desc.typeName -- we home in on object's typeName @@ -93,7 +96,10 @@ function cfxObjectDestructDetector:onEvent(event) matchMe = string.upper(matchMe) for idx, aZone in pairs(cfxObjectDestructDetector.objectZones) do - if (not aZone.isDestroyed) and aZone.objName == matchMe then + if (not aZone.isDestroyed) + and aZone.objName == matchMe + and aZone:pointInZone(pos) -- make sure it's not a dupe + then if aZone.outDestroyFlag then aZone:pollFlag(aZone.outDestroyFlag, aZone.oddMethod) end diff --git a/modules/cfxObjectSpawnZones.lua b/modules/objectSpawnZones.lua similarity index 95% rename from modules/cfxObjectSpawnZones.lua rename to modules/objectSpawnZones.lua index 43a536e..7024db1 100644 --- a/modules/cfxObjectSpawnZones.lua +++ b/modules/objectSpawnZones.lua @@ -13,25 +13,6 @@ cfxObjectSpawnZones.verbose = false *** DOES NOT EXTEND ZONES *** version history - 1.0.0 - based on 1.4.6 version from cfxSpawnZones - 1.1.0 - uses linkedUnit, so spawnming can occur on ships - make sure you also enable useOffset to place the - statics away from the center of the ship - 1.1.1 - also processes paused flag - - despawnRemaining(spawner) - 1.1.2 - autoRemove option re-installed - - added possibility to autoUnlink - 1.1.3 - ME-triggered flag via f? and triggerFlag - 1.1.4 - activate?, pause? attributes - 1.1.5 - spawn?, spawnObjects? synonyms - 1.2.0 - DML flag upgrade - 1.2.1 - config zone - - autoLink bug (zone instead of spawner accessed) - 1.3.0 - better synonym handling - - useDelicates link to delicate when spawned - - spawned single and multi-objects can be made delicates - 1.3.1 - baseName can be set to zone's name by giving "*" - 1.3.2 - delicateName supports '*' to refer to own zone 2.0.0 - dmlZones --]]-- diff --git a/modules/cfxOwnedZones (legacy 1.3.0).lua b/modules/ownedZones (legacy 1.3.0).lua similarity index 100% rename from modules/cfxOwnedZones (legacy 1.3.0).lua rename to modules/ownedZones (legacy 1.3.0).lua diff --git a/modules/cfxOwnedZones.lua b/modules/ownedZones.lua similarity index 100% rename from modules/cfxOwnedZones.lua rename to modules/ownedZones.lua diff --git a/modules/persistence.lua b/modules/persistence.lua index a9fae97..f509754 100644 --- a/modules/persistence.lua +++ b/modules/persistence.lua @@ -1,5 +1,5 @@ persistence = {} -persistence.version = "1.0.7" +persistence.version = "2.0.0" persistence.ups = 1 -- once every 1 seconds persistence.verbose = false persistence.active = false @@ -10,39 +10,25 @@ persistence.saveDir = nil -- set at start persistence.name = "persistence" -- for cfxZones persistence.missionData = {} -- loaded from file persistence.requiredLibs = { - "dcsCommon", -- always - "cfxZones", -- Zones, of course + "dcsCommon", + "cfxZones", } --[[-- Version History - 1.0.0 - initial version - 1.0.1 - when available, module sets flag "cfxPersistence" to 1 - - when data availabe, cfxPersistenceHasData is set to 1 - - spelling - - cfxZones interface - - always output save location - 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 - 1.0.5 - desanitize check on readConfig to early-abort - 1.0.6 - removed potential verbosity bug - 1.0.7 - correct abort for sanitized DCS, when non-verbose - + 2.0.0 - dml zones, OOP + cleanup PROVIDES LOAD/SAVE ABILITY TO MODULES PROVIDES STANDALONE/HOSTED SERVER COMPATIBILITY - --]]-- --- in order to work, Host must desanitize lfs and io --- only works when run as server +-- in order to work, HOST MUST DESANITIZE lfs and io -- -- flags to save. can be added to by saveFlags attribute -- persistence.flagsToSave = {} -- simple table -persistence.callbacks = {} -- cbblocks, dictionary +persistence.callbacks = {} -- cbblocks, dictionary by name -- @@ -417,13 +403,13 @@ end -- function persistence.collectFlagsFromZone(theZone) - local theFlags = cfxZones.getStringFromZoneProperty(theZone, "saveFlags", "*dummy") + local theFlags = theZone:getStringFromZoneProperty("saveFlags", "*dummy") persistence.registerFlagsToSave(theFlags, theZone) end function persistence.readConfigZone() if not _G["lfs"] then - trigger.action.outText("+++persistence: DCS not correctly desanitized. Persistence disabled", 30) + trigger.action.outText("+++persistence: DCS correctly not 'desanitized'. Persistence disabled", 30) return end @@ -436,10 +422,10 @@ function persistence.readConfigZone() -- serverDir is the path from the server save directory, usually "Missions/". -- will be added to lfs.writedir() unless given a root attribute - if cfxZones.hasProperty(theZone, "root") then + if theZone:hasProperty("root") then -- we split this to enable further processing down the -- line if neccessary - persistence.root = cfxZones.getStringFromZoneProperty(theZone, "root", lfs.writedir()) -- safe default + persistence.root = theZone:getStringFromZoneProperty("root", lfs.writedir()) -- safe default if not dcsCommon.stringEndsWith(persistence.root, "\\") then persistence.root = persistence.root .. "\\" end @@ -453,11 +439,11 @@ function persistence.readConfigZone() end end - persistence.serverDir = cfxZones.getStringFromZoneProperty(theZone, "serverDir", "Missions\\") + persistence.serverDir = theZone:getStringFromZoneProperty("serverDir", "Missions\\") if hasConfig then - if cfxZones.hasProperty(theZone, "saveDir") then - persistence.saveDir = cfxZones.getStringFromZoneProperty(theZone, "saveDir", "") + if theZone:hasProperty("saveDir") then + persistence.saveDir = theZone:getStringFromZoneProperty("saveDir", "") else -- local missname = net.dostring_in("gui", "return DCS.getMissionName()") .. " (data)" persistence.saveDir = dcsCommon.getMissionName() .. " (data)" @@ -472,32 +458,32 @@ function persistence.readConfigZone() trigger.action.outText("*** WARNING: persistence is set to write to main mission directory!", 30) end - if cfxZones.hasProperty(theZone, "saveFileName") then - persistence.saveFileName = cfxZones.getStringFromZoneProperty(theZone, "saveFileName", dcsCommon.getMissionName() .. " Data.txt") + if theZone:hasProperty("saveFileName") then + persistence.saveFileName = theZone:getStringFromZoneProperty("saveFileName", dcsCommon.getMissionName() .. " Data.txt") end - if cfxZones.hasProperty(theZone, "versionID") then - persistence.versionID = cfxZones.getStringFromZoneProperty(theZone, "versionID", "") -- to check for full restart + if theZone:hasProperty("versionID") then + persistence.versionID = theZone:getStringFromZoneProperty("versionID", "") -- to check for full restart end - persistence.saveInterval = cfxZones.getNumberFromZoneProperty(theZone, "saveInterval", -1) -- default to manual save + persistence.saveInterval = theZone:getNumberFromZoneProperty("saveInterval", -1) -- default to manual save if persistence.saveInterval > 0 then persistence.saveTime = persistence.saveInterval * 60 + timer.getTime() end - if cfxZones.hasProperty(theZone, "cleanRestart?") then - persistence.cleanRestart = cfxZones.getStringFromZoneProperty(theZone, "cleanRestart?", "*") - persistence.lastCleanRestart = cfxZones.getFlagValue(persistence.cleanRestart, theZone) + if theZone:hasProperty("cleanRestart?") then + persistence.cleanRestart = theZone:getStringFromZoneProperty("cleanRestart?", "*") + persistence.lastCleanRestart = theZone:getFlagValue(persistence.cleanRestart) end - if cfxZones.hasProperty(theZone, "saveMission?") then - persistence.saveMission = cfxZones.getStringFromZoneProperty(theZone, "saveMission?", "*") - persistence.lastSaveMission = cfxZones.getFlagValue(persistence.saveMission, theZone) + if theZone:hasProperty("saveMission?") then + persistence.saveMission = theZone:getStringFromZoneProperty("saveMission?", "*") + persistence.lastSaveMission = theZone:getFlagValue(persistence.saveMission) end - persistence.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + persistence.verbose = theZone.verbose - persistence.saveNotification = cfxZones.getBoolFromZoneProperty(theZone, "saveNotification", true) + persistence.saveNotification = theZone:getBoolFromZoneProperty("saveNotification", true) if persistence.verbose then trigger.action.outText("+++persistence: read config", 30) @@ -534,14 +520,13 @@ function persistence.start() return false end --- 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 - + -- save directory if persistence.isDir(mainDir) then if persistence.verbose then trigger.action.outText("persistence: main dir is <" .. mainDir .. ">", 30) @@ -613,7 +598,6 @@ function persistence.start() -- and start updating persistence.update() - return persistence.active end @@ -627,5 +611,3 @@ if not persistence.start() then end -- we do NOT remove the methods so we don't crash end - --- add zones for saveFlags so authors can easily save flag values diff --git a/modules/cfxPlayerScore.lua b/modules/playerScore.lua similarity index 88% rename from modules/cfxPlayerScore.lua rename to modules/playerScore.lua index 161f9bb..8801727 100644 --- a/modules/cfxPlayerScore.lua +++ b/modules/playerScore.lua @@ -1,91 +1,17 @@ cfxPlayerScore = {} -cfxPlayerScore.version = "3.0.0" +cfxPlayerScore.version = "3.0.1" cfxPlayerScore.name = "cfxPlayerScore" -- compatibility with flag bangers cfxPlayerScore.badSound = "Death BRASS.wav" cfxPlayerScore.scoreSound = "Quest Snare 3.wav" cfxPlayerScore.announcer = true cfxPlayerScore.firstSave = true -- to force overwrite --[[-- VERSION HISTORY - 1.0.1 - bug fixes to killDetected - 1.0.2 - messaging clean-up, less verbose - 1.1.0 - integrated score base system - - accepts configZones - - module validation - - isNamedUnit(theUnit) - - notify if named unit killed - - kill weapon reported - 1.2.0 - score table - - announcer attribute - - badSound name - - scoreSound name - 1.3.0 - object2score - - static objects also can score - - can now also score members of group by adding group name - - scenery objects are now supported. use the - number that is given under OBJECT ID when - using assign as... - 1.3.1 - isStaticObject() to better detect buildings after Match 22 patch - 1.3.2 - corrected ground default score - - removed dependency to cfxPlayer - 1.4.0 - persistence support - - better unit-->static switch support for generic type kill - 1.5.0 - added feats to score - - feats API - - logFeatForPlayer(playerName, theFeat, coa) - 1.5.1 - init feats before reading - 2.0.0 - sound name for good and bad corrected - - scoreOnly option - - saveScore? input in config - - incremental option in config for save - - preProcessor() added land and birth for players - - addSafeZone() - - landing score possible as feat - - also detect landing and birth events for players - - birth zeroes deferred scores and feats - - delayAfterLanding property - - delayBetweenLandings to filter landings and - space them properly from take offs - - ranking option - - scoreOnly option - - scoreFileName option - - update() loop - - read "scoreSafe" zones - - scoreaccu in playerScore - - landing feat added, enabled with landing > 0 score attribute - - delayed awards after landing - - save score to file - - show score to all - - feat zones - - awardLimit - feats can be limited in number total - - wildcard support for feature - - kill zones - limit kill score awards to inside zones - - feats can be limited to once per player - - persistence: featNum - - persistence: awardedTo - - always schedule - - hasAward logic - - unit2player - - detect player plane death - - ffMod attribute - - pkMod attribute - - pvp feat - - immediate awarding of all negative scores, even if deferred - 2.0.1 - corrected access to nowString() - - more robust config reading - 2.1.0 - coalition score - - reportCoalition switch - - persist coalition score - - add score to coalition when scoring player - 2.1.1 - check ownership of scoreSafe zone upon touch-down - - new scoreSummaryForPlayersOfCoalition() - - new noGrief option in config - - improved guards when checking ownership (nil zone owner) - 2.2.0 - score flags for red and blue 3.0.0 - dmlFlags OOP - redScore# - blueScore# - sceneryObject detection improvements - DCS 2.9 safe + 3.0.1 - cleanup --]]-- @@ -109,12 +35,6 @@ cfxPlayerScore.killZones = {} -- when set, kills only count here cfxPlayerScore.typeScore = {} cfxPlayerScore.lastPlayerLanding = {} -- timestamp, by player name cfxPlayerScore.delayBetweenLandings = 30 -- seconds to count as separate landings, also set during take-off to prevent janky t/o to count. --- --- we subscribe to the kill event. each time a unit --- is killed, we check if it was killed by a player --- and if so, that player record is updated and the side --- whom the player belongs to is informed --- cfxPlayerScore.aircraft = 50 cfxPlayerScore.helo = 40 cfxPlayerScore.ground = 10 @@ -655,7 +575,6 @@ function cfxPlayerScore.linkUnitWithPlayer(theUnit) local uName = theUnit:getName() local pName = theUnit:getPlayerName() cfxPlayerScore.unit2player[uName] = pName - --cfxPlayerScore.player2unit[pName] = uName -- is this needed? end function cfxPlayerScore.unlinkUnit(theUnit) @@ -790,7 +709,6 @@ function cfxPlayerScore.checkKillFeat(name, killer, victim, fratricide) local killFeats = cfxPlayerScore.featsForLocation(name, theLoc, coa,"KILL", killer, victim) if (not fratricide) and #killFeats > 0 then - -- use the feat description -- we may want to use closest, currently simply the first theFeatZone = killFeats[1] @@ -826,7 +744,6 @@ function cfxPlayerScore.killDetected(theEvent) if wasBuilding then -- these objects have no coalition; we simply award the score if -- it exists in look-up table. - --trigger.action.outText("KILL SCENERY", 30) local staticScore = cfxPlayerScore.object2score(victim) if staticScore > 0 then trigger.action.outSoundForCoalition(killSide, cfxPlayerScore.scoreSound) @@ -847,10 +764,9 @@ function cfxPlayerScore.killDetected(theEvent) local isStO = cfxPlayerScore.isStaticObject(victim) --if not victim.getGroup then if isStO then - -- static objects have no group - + -- static objects have no group local staticName = victim:getName() -- on statics, this returns - -- name as entered in TOP LINE + -- name as entered in TOP LINE local staticScore = cfxPlayerScore.object2score(victim) if staticScore > 0 then @@ -870,9 +786,7 @@ function cfxPlayerScore.killDetected(theEvent) if not fraternicide then cfxPlayerScore.checkKillFeat(killerName, killer, victim, false) - end - - + end return end @@ -881,7 +795,7 @@ function cfxPlayerScore.killDetected(theEvent) trigger.action.outText("+++scr: strange stuff:group, outta here", 30) return end - local vicCat = vicGroup:getCategory() + local vicCat = vicGroup:getCategory() -- group cat is DCS 2.9 safe if not vicCat then trigger.action.outText("+++scr: strange stuff:cat, outta here", 30) return @@ -889,25 +803,24 @@ function cfxPlayerScore.killDetected(theEvent) local unitScore = cfxPlayerScore.unit2score(victim) -- see which weapon was used. gun kills score 2x - local killMeth = "" - local killWeap = theEvent.weapon +-- local killMeth = "" -- meth is currently not defined +-- local killWeap = theEvent.weapon -- not supported either - if pk then + if pk then -- player kill - add player's name vicDesc = victim:getPlayerName() .. " in " .. vicDesc scoreMod = scoreMod * cfxPlayerScore.pkMod end - -- if fratricide, times ffMod (friedlyFire) if fraternicide then scoreMod = scoreMod * cfxPlayerScore.ffMod ---2 if cfxPlayerScore.announcer then - trigger.action.outTextForCoalition(killSide, killerName .. " in " .. killVehicle .. " killed FRIENDLY " .. vicDesc .. killMeth .. "!", 30) + trigger.action.outTextForCoalition(killSide, killerName .. " in " .. killVehicle .. " killed FRIENDLY " .. vicDesc .. "!", 30) trigger.action.outSoundForCoalition(killSide, cfxPlayerScore.badSound) end else if cfxPlayerScore.announcer then - trigger.action.outText(killerName .. " in " .. killVehicle .." killed " .. vicDesc .. killMeth .."!", 30) + trigger.action.outText(killerName .. " in " .. killVehicle .." killed " .. vicDesc .. "!", 30) trigger.action.outSoundForCoalition(vicSide, cfxPlayerScore.badSound) trigger.action.outSoundForCoalition(killSide, cfxPlayerScore.scoreSound) end @@ -960,7 +873,6 @@ function cfxPlayerScore.handlePlayerLanding(theEvent) -- we may want to use closest, currently simply the first theFeatZone = landingFeats[1] desc = cfxPlayerScore.evalFeatDescription(playerName, theFeatZone, thePlayerUnit) -- nil victim, defaults to player - else if theEvent.place then desc = desc .. " successfully (" .. theEvent.place:getName() .. ")" @@ -1067,8 +979,7 @@ function cfxPlayerScore.scheduledAward(args) trigger.action.outTextForUnit(uid, "Can't award score for <" .. playerName .. ">, not in safe zone.", 30) return end - - + local theScore = cfxPlayerScore.getPlayerScore(playerName) local playerSide = dcsCommon.playerName2Coalition(playerName) if playerSide < 1 then @@ -1214,59 +1125,58 @@ function cfxPlayerScore.handlePlayerEvent(theEvent) end function cfxPlayerScore.readConfigZone(theZone) - cfxPlayerScore.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + cfxPlayerScore.verbose = theZone.verbose -- default scores - cfxPlayerScore.aircraft = cfxZones.getNumberFromZoneProperty(theZone, "aircraft", 50) - cfxPlayerScore.helo = cfxZones.getNumberFromZoneProperty(theZone, "helo", 40) - cfxPlayerScore.ground = cfxZones.getNumberFromZoneProperty(theZone, "ground", 10) - cfxPlayerScore.ship = cfxZones.getNumberFromZoneProperty(theZone, "ship", 80) - cfxPlayerScore.train = cfxZones.getNumberFromZoneProperty(theZone, "train", 5) - cfxPlayerScore.landing = cfxZones.getNumberFromZoneProperty(theZone, "landing", 0) -- if > 0 then feat + cfxPlayerScore.aircraft = theZone:getNumberFromZoneProperty("aircraft", 50) + cfxPlayerScore.helo = theZone:getNumberFromZoneProperty("helo", 40) + cfxPlayerScore.ground = theZone:getNumberFromZoneProperty("ground", 10) + cfxPlayerScore.ship = theZone:getNumberFromZoneProperty("ship", 80) + cfxPlayerScore.train = theZone:getNumberFromZoneProperty( "train", 5) + cfxPlayerScore.landing = theZone:getNumberFromZoneProperty("landing", 0) -- if > 0 then feat - cfxPlayerScore.pkMod = cfxZones.getNumberFromZoneProperty(theZone, "pkMod", 1) -- factor for killing a player - cfxPlayerScore.ffMod = cfxZones.getNumberFromZoneProperty(theZone, "ffMod", -2) -- factor for friendly fire - cfxPlayerScore.planeLoss = cfxZones.getNumberFromZoneProperty(theZone, "planeLoss", -10) -- points added when player's plane crashes + cfxPlayerScore.pkMod = theZone:getNumberFromZoneProperty( "pkMod", 1) -- factor for killing a player + cfxPlayerScore.ffMod = theZone:getNumberFromZoneProperty( "ffMod", -2) -- factor for friendly fire + cfxPlayerScore.planeLoss = theZone:getNumberFromZoneProperty("planeLoss", -10) -- points added when player's plane crashes - cfxPlayerScore.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true) + cfxPlayerScore.announcer = theZone:getBoolFromZoneProperty("announcer", true) - if cfxZones.hasProperty(theZone, "badSound") then - cfxPlayerScore.badSound = cfxZones.getStringFromZoneProperty(theZone, "badSound", "") + if theZone:hasProperty("badSound") then + cfxPlayerScore.badSound = theZone:getStringFromZoneProperty("badSound", "") end - if cfxZones.hasProperty(theZone, "scoreSound") then - cfxPlayerScore.scoreSound = cfxZones.getStringFromZoneProperty(theZone, "scoreSound", "") + if theZone:hasProperty("scoreSound") then + cfxPlayerScore.scoreSound = theZone:getStringFromZoneProperty("scoreSound", "") end -- triggering saving scores - if cfxZones.hasProperty(theZone, "saveScore?") then - cfxPlayerScore.saveScore = cfxZones.getStringFromZoneProperty(theZone, "saveScore?", "none") + if theZone:hasProperty("saveScore?") then + cfxPlayerScore.saveScore = theZone:getStringFromZoneProperty("saveScore?", "none") cfxPlayerScore.lastSaveScore = trigger.misc.getUserFlag(cfxPlayerScore.saveScore) - cfxPlayerScore.incremental = cfxZones.getBoolFromZoneProperty(theZone, "incremental", false) -- incremental saves + cfxPlayerScore.incremental = theZone:getBoolFromZoneProperty("incremental", false) -- incremental saves end -- triggering show all scores - if cfxZones.hasProperty(theZone, "showScore?") then - cfxPlayerScore.showScore = cfxZones.getStringFromZoneProperty(theZone, "showScore?", "none") + if theZone:hasProperty("showScore?") then + cfxPlayerScore.showScore = theZone:getStringFromZoneProperty("showScore?", "none") cfxPlayerScore.lastShowScore = trigger.misc.getUserFlag(cfxPlayerScore.showScore) end - cfxPlayerScore.rankPlayers = cfxZones.getBoolFromZoneProperty(theZone, "rankPlayers", false) + cfxPlayerScore.rankPlayers = theZone:getBoolFromZoneProperty("rankPlayers", false) - cfxPlayerScore.scoreOnly = cfxZones.getBoolFromZoneProperty(theZone, "scoreOnly", true) + cfxPlayerScore.scoreOnly = theZone:getBoolFromZoneProperty("scoreOnly", true) - cfxPlayerScore.deferred = cfxZones.getBoolFromZoneProperty(theZone, "deferred", false) + cfxPlayerScore.deferred = theZone:getBoolFromZoneProperty("deferred", false) - cfxPlayerScore.delayAfterLanding = cfxZones.getNumberFromZoneProperty(theZone, "delayAfterLanding", 10) + cfxPlayerScore.delayAfterLanding = theZone:getNumberFromZoneProperty("delayAfterLanding", 10) - cfxPlayerScore.scoreFileName = cfxZones.getStringFromZoneProperty(theZone, "scoreFileName", "Player Scores") + cfxPlayerScore.scoreFileName = theZone:getStringFromZoneProperty("scoreFileName", "Player Scores") - cfxPlayerScore.reportScore = cfxZones.getBoolFromZoneProperty(theZone, "reportScore", true) + cfxPlayerScore.reportScore = theZone:getBoolFromZoneProperty("reportScore", true) - cfxPlayerScore.reportFeats = cfxZones.getBoolFromZoneProperty(theZone, "reportFeats", true) + cfxPlayerScore.reportFeats = theZone:getBoolFromZoneProperty("reportFeats", true) - cfxPlayerScore.reportCoalition = cfxZones.getBoolFromZoneProperty( - theZone, "reportCoalition", false) -- also show coalition score + cfxPlayerScore.reportCoalition = theZone:getBoolFromZoneProperty("reportCoalition", false) -- also show coalition score - cfxPlayerScore.noGrief = cfxZones.getBoolFromZoneProperty(theZone, "noGrief", true) -- noGrief = only add positive score + cfxPlayerScore.noGrief = theZone:getBoolFromZoneProperty( "noGrief", true) -- noGrief = only add positive score if theZone:hasProperty("redScore#") then cfxPlayerScore.redScoreOut = theZone:getStringFromZoneProperty("redScore#") diff --git a/modules/cfxPlayerScoreUI.lua b/modules/playerScoreUI.lua similarity index 97% rename from modules/cfxPlayerScoreUI.lua rename to modules/playerScoreUI.lua index 291ee07..88103dc 100644 --- a/modules/cfxPlayerScoreUI.lua +++ b/modules/playerScoreUI.lua @@ -10,6 +10,7 @@ cfxPlayerScoreUI.verbose = false - 2.1.0 - soundfile cleanup - score summary for side - allowAll + - 2.1.1 - minor cleanup --]]-- cfxPlayerScoreUI.requiredLibs = { @@ -116,7 +117,7 @@ function cfxPlayerScoreUI.start() world.addEventHandler(cfxPlayerScoreUI) -- process all existing players (late start) dcsCommon.iteratePlayers(cfxPlayerScore.processPlayerUnit) - trigger.action.outText("cf/x cfxPlayerScoreUI v" .. cfxPlayerScoreUI.version .. " started", 30) + trigger.action.outText("cf/x PlayerScoreUI v" .. cfxPlayerScoreUI.version .. " started", 30) return true end diff --git a/modules/playerZone.lua b/modules/playerZone.lua index bb9b825..42a281c 100644 --- a/modules/playerZone.lua +++ b/modules/playerZone.lua @@ -1,40 +1,39 @@ playerZone = {} -playerZone.version = "1.0.0" +playerZone.version = "2.0.0" playerZone.requiredLibs = { - "dcsCommon", -- always - "cfxZones", -- Zones, of course + "dcsCommon", + "cfxZones", } playerZone.playerZones = {} --[[-- Version History 1.0.0 - Initial version 1.0.1 - pNum --> pNum# + 2.0.0 - dmlZones + red#, blue#, total# --]]-- function playerZone.createPlayerZone(theZone) -- start val - a range - theZone.pzCoalition = cfxZones.getCoalitionFromZoneProperty(theZone, "playerZone", 0) - - + theZone.pzCoalition = theZone:getCoalitionFromZoneProperty( "playerZone", 0) -- Method for outputs - theZone.pzMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") - if cfxZones.hasProperty(theZone, "pzMethod") then - theZone.pzMethod = cfxZones.getStringFromZoneProperty(theZone, "pwMethod", "inc") + + theZone.pzMethod = theZone:getStringFromZoneProperty("method", "inc") + if theZone:hasProperty("pzMethod") then + theZone.pzMethod = theZone:getStringFromZoneProperty("pwMethod", "inc") end - if cfxZones.hasProperty(theZone, "pNum#") then - theZone.pNum = cfxZones.getStringFromZoneProperty(theZone, "pNum#", "none") - elseif cfxZones.hasProperty(theZone, "pNum") then - theZone.pNum = cfxZones.getStringFromZoneProperty(theZone, "pNum", "none") + if theZone:hasProperty("pNum#") then + theZone.pNum = theZone:getStringFromZoneProperty("pNum#", "none") end - if cfxZones.hasProperty(theZone, "added!") then - theZone.pAdd = cfxZones.getStringFromZoneProperty(theZone, "added!", "none") + if theZone:hasProperty("added!") then + theZone.pAdd = theZone:getStringFromZoneProperty("added!", "none") end - if cfxZones.hasProperty(theZone, "gone!") then - theZone.pRemove = cfxZones.getStringFromZoneProperty(theZone, "gone!", "none") + if theZone:hasProperty("gone!") then + theZone.pRemove = theZone:getStringFromZoneProperty("gone!", "none") end theZone.playersInZone = {} -- indexed by unit name @@ -49,7 +48,7 @@ function playerZone.collectPlayersForZone(theZone) local allPlayers = coalition.getPlayers(f) for idy, theUnit in pairs (allPlayers) do local loc = theUnit:getPoint() - if cfxZones.pointInZone(loc, theZone) then + if theZone:pointInZone(loc) then zonePlayers[theUnit:getName()] = theUnit end end @@ -86,21 +85,21 @@ function playerZone.processZone(theZone) -- flag handling and banging if theZone.pNum then - cfxZones.setFlagValueMult(theZone.pNum, newCount, theZone) + theZone:setFlagValue(theZone.pNum, newCount) end if theZone.pAdd and hasNew then if theZone.verbose or playerZone.verbose then trigger.action.outText("+++pZone: banging <" .. theZone.name .. ">'s 'added!' flags <" .. theZone.pAdd .. ">", 30) end - cfxZones.pollFlag(theZone.pAdd, theZone.pzMethod, theZone) + theZone:pollFlag(theZone.pAdd, theZone.pzMethod) end if theZone.pRemove and hasGone then if theZone.verbose or playerZone.verbose then trigger.action.outText("+++pZone: banging <" .. theZone.name .. ">'s 'gone' flags <" .. theZone.pRemove .. ">", 30) end - cfxZones.pollFlag(theZone.pAdd, theZone.pzMethod, theZone) + theZone:pollFlag(theZone.pAdd, theZone.pzMethod) end end @@ -113,6 +112,25 @@ function playerZone.update() -- iterate all zones and check them for idx, theZone in pairs(playerZone.playerZones) do + local neutrals = coalition.getPlayers(0) + local neutralNum = #neutrals + local reds = coalition.getPlayers(1) + local redNum = #reds + local blues = coalition.getPlayers(2) + local blueNum = #blues + local totalNum = neutralNum + redNum + blueNum + if playerZone.neutralNum then + cfxZones.setFlagValue(playerZone.neutralNum, neutralNum, playerZone) + end + if playerZone.redNum then + cfxZones.setFlagValue(playerZone.redNum, redNum, playerZone) + end + if playerZone.blueNum then + cfxZones.setFlagValue(playerZone.blueNum, blueNum, playerZone) + end + if playerZone.totalNum then + cfxZones.setFlagValue(playerZone.totalNum, totalNum, playerZone) + end playerZone.processZone(theZone) end end @@ -121,7 +139,20 @@ end -- Read Config Zone -- function playerZone.readConfigZone(theZone) + playerZone.name = "playerZoneConfig" -- cfxZones compat -- currently nothing to do + if theZone:hasProperty("red#") then + playerZone.redNum = theZone:getStringFromZoneProperty("red#", "none") + end + if theZone:hasProperty("blue#") then + playerZone.blueNum = theZone:getStringFromZoneProperty("blue#", "none") + end + if theZone:hasProperty("neutral#") then + playerZone.neutralNum = theZone:getStringFromZoneProperty("neutral#", "none") + end + if theZone:hasProperty("total#") then + playerZone.totalNum = theZone:getStringFromZoneProperty("total#", "none") + end end -- diff --git a/modules/pulseFlags.lua b/modules/pulseFlags.lua index cdb2ac5..df5da57 100644 --- a/modules/pulseFlags.lua +++ b/modules/pulseFlags.lua @@ -11,34 +11,6 @@ pulseFlags.requiredLibs = { Copyright 2022 by Christian Franz and cf/x Version History - - 1.0.0 Initial version - - 1.0.1 pause behavior debugged - - 1.0.2 zero pulse optional initial pulse suppress - - 1.0.3 pollFlag switched to cfxZones - uses randomDelayFromPositiveRange - flag! now is string - WARNING: still needs full alphaNum flag upgrade - - 1.1.0 Full DML flag integration - removed zone! - made pulse and pulse! the out flag carrier - done! - pulsesDone! synonym - pausePulse? synonym - pulseMethod synonym - startPulse? synonym - pulseStopped synonym - - 1.2.0 DML Watchflag integration - corrected bug in loading last pulse value for paused - - 1.2.1 pulseInterval synonym for time - pulses now supports range - zone-local verbosity - - 1.2.2 outputMethod synonym - - 1.2.3 deprecated paused/pulsePaused - returned onStart, defaulting to true - - 1.3.0 persistence - - 1.3.1 typos corrected - - 1.3.2 removed last pulse's timeID upon entry in doPulse - - 1.3.3 removed 'pulsing' when pausing, so we can restart - 2.0.0 dmlZones / OOP using method on all outputs - 2.0.1 activateZoneFlag now works correctly @@ -165,7 +137,7 @@ function pulseFlags.doPulse(args) -- first, we only do an initial pulse if zeroPulse is set if theZone.hasPulsed or theZone.zeroPulse then if pulseFlags.verbose or theZone.verbose then - trigger.action.outText("+++pulF: will bang " .. theZone.pulseFlag, 30); + trigger.action.outText("+++pulF: will bang " .. theZone.pulseFlag .. " for <" .. theZone.name .. ">", 30); end theZone:pollFlag(theZone.pulseFlag, theZone.pulseMethod) diff --git a/modules/radioMenus.lua b/modules/radioMenus.lua index 70f61d0..dd9cafd 100644 --- a/modules/radioMenus.lua +++ b/modules/radioMenus.lua @@ -1,5 +1,5 @@ radioMenu = {} -radioMenu.version = "2.1.1" +radioMenu.version = "2.2.0" radioMenu.verbose = false radioMenu.ups = 1 radioMenu.requiredLibs = { @@ -10,21 +10,6 @@ radioMenu.menus = {} --[[-- Version History - 1.0.0 - Initial version - 1.0.1 - spelling corrections - 1.1.0 - removeMenu - addMenu - menuVisible - 2.0.0 - redesign: handles multiple receivers - optional MX module - group option - type option - multiple group names - multiple types - gereric helo type - generic plane type - type works with coalition - 2.0.1 - corrections to installMenu(), as suggested by GumidekCZ 2.1.0 - valA/valB/valC/valD attributes OOP cfxZones corrected CD setting for "D" @@ -32,6 +17,7 @@ radioMenu.menus = {} valA-D now define full method, not just values full wildcard support for ack and cooldown 2.1.1 - outMessage now works correctly + 2.2.0 - clean-up --]]-- function radioMenu.addRadioMenu(theZone) @@ -63,8 +49,7 @@ function radioMenu.filterPlayerIDForType(theZone) end -- now iterate all types, and include any player that matches - -- note that a player may match twice, so we use a dict instead of an - -- array. Since we later iterate ID by idx, that's not an issue + -- note that players may match twice, so we use a dict for idx, aType in pairs(allTypes) do local theType = dcsCommon.trim(aType) @@ -145,7 +130,6 @@ function radioMenu.filterPlayerIDForGroup(theZone) end function radioMenu.installMenu(theZone) --- local theGroup = 0 -- was: nil local gID = nil if theZone.menuGroup then if not cfxMX then @@ -570,6 +554,5 @@ if not radioMenu.start() then end --[[-- - callbacks for the menus check CD/standby code for multiple groups --]]-- \ No newline at end of file diff --git a/modules/cfxReconGUI.lua b/modules/reconGUI.lua similarity index 100% rename from modules/cfxReconGUI.lua rename to modules/reconGUI.lua diff --git a/modules/cfxReconMode.lua b/modules/reconMode.lua similarity index 100% rename from modules/cfxReconMode.lua rename to modules/reconMode.lua diff --git a/modules/cfxSmokeZones.lua b/modules/smokeZones.lua similarity index 84% rename from modules/cfxSmokeZones.lua rename to modules/smokeZones.lua index 0832749..f2ab60d 100644 --- a/modules/cfxSmokeZones.lua +++ b/modules/smokeZones.lua @@ -1,25 +1,12 @@ cfxSmokeZone = {} -cfxSmokeZone.version = "1.2.0" +cfxSmokeZone.version = "2.0.0" cfxSmokeZone.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course } --[[-- Version History - 1.0.0 - initial version - 1.0.1 - added removeSmokeZone - 1.0.2 - added altitude - 1.0.3 - added paused attribute - - added f? attribute --> onFlag - - broke out startSmoke - 1.0.4 - startSmoke? synonym - - alphanum DML flag upgrade - - random color support - 1.1.0 - Watchflag upgrade - 1.1.1 - stopSmoke? input - 1.1.2 - 'agl', 'alt' synonymous for altitude to keep in line with fireFX - 1.1.3 - corrected smokeTriggerMethod in zone definition - 1.2.0 - first OOP guinea pig. + 2.0.0 - clean up --]]-- cfxSmokeZone.smokeZones = {} @@ -48,7 +35,6 @@ function cfxSmokeZone.processSmokeZone(aZone) -- paused aZone.paused = aZone:getBoolFromZoneProperty("paused", false) - -- f? query flags if aZone:hasProperty("f?") then aZone.onFlag = aZone:getStringFromZoneProperty("f?", "*") elseif aZone:hasProperty("startSmoke?") then @@ -64,8 +50,6 @@ function cfxSmokeZone.processSmokeZone(aZone) aZone.smkLastStopFlag = aZone:getFlagValue(aZone.smkStopFlag) end - -- watchflags: - -- triggerMethod aZone.smokeTriggerMethod = aZone:getStringFromZoneProperty( "triggerMethod", "change") if aZone:hasProperty("smokeTriggerMethod") then @@ -165,14 +149,10 @@ function cfxSmokeZone.start() end -- collect all zones with 'smoke' attribute - -- collect all spawn zones local attrZones = cfxZones.getZonesWithAttributeNamed("smoke") - - -- now create a smoker for all, add them to updater, - -- smoke all that aren't paused - for k, aZone in pairs(attrZones) do - cfxSmokeZone.processSmokeZone(aZone) -- process attribute and add to zone - cfxSmokeZone.addSmokeZone(aZone) -- remember it so we can smoke it + for k, aZone in pairs(attrZones) do + cfxSmokeZone.processSmokeZone(aZone) + cfxSmokeZone.addSmokeZone(aZone) end -- start update loop diff --git a/modules/cfxSpawnZones.lua b/modules/spawnZones.lua similarity index 91% rename from modules/cfxSpawnZones.lua rename to modules/spawnZones.lua index 97ca160..48ac9a0 100644 --- a/modules/cfxSpawnZones.lua +++ b/modules/spawnZones.lua @@ -21,53 +21,7 @@ cfxSpawnZones.spawnedGroups = {} -- *** DOES NOT EXTEND ZONES *** LINKED OWNER via masterOwner *** -- --[[-- --- version history - 1.3.0 - - maxSpawn - - orders - - range - 1.3.1 - spawnWithSpawner correct translation of country to coalition - - createSpawner - corrected reading from properties - 1.3.2 - createSpawner - correct reading 'owner' from properties, now - directly reads coalition - 1.4.0 - checks modules - - orders 'train' or 'training' - will make the - ground troops be issued HOLD WEAPS and - not added to any queue. 'Training' troops - are target dummies. - - optional heading attribute - - typeMult: repeate type this many time (can produce army in one call) - 1.4.1 - 'requestable' attribute. will automatically set zone to - - paused, so troops can be produced on call - - getRequestableSpawnersInRange - 1.4.2 - target attribute. used for - - orders: attackZone - - spawner internally copies name from cfxZone used for spawning (convenience only) - 1.4.3 - can subscribe to callbacks. currently called when spawnForSpawner is invoked, reason is "spawned" - - masterOwner to link ownership to other zone - 1.4.4 - autoRemove flag to instantly start CD and respawn - 1.4.5 - verify that maxSpawns ~= 0 on initial spawn on start-up - 1.4.6 - getSpawnerForZoneNamed(aName) - - nil-trapping orders before testing for 'training' - 1.4.7 - defaulting orders to 'guard' - - also accept 'dummy' and 'dummies' as substitute for training - 1.4.8 - spawnWithSpawner uses getPoint to support linked spawn zones - - update spawn count on initial spawn - 1.5.0 - f? support to trigger spawn - - spawnWithSpawner made string compatible - 1.5.1 - relaxed baseName and default to dcsCommon.uuid() - - verbose - 1.5.2 - activate?, pause? flag - 1.5.3 - spawn?, spawnUnits? flags - 1.6.0 - trackwith interface for group tracker - 1.7.0 - persistence support - 1.7.1 - improved verbosity - - spelling check - 1.7.2 - baseName now can can be set to zone name by issuing "*" - 1.7.3 - ability to hand off to delicates, useDelicates attribute - 1.7.4 - wait-attackZone fixes - 1.7.5 - improved verbosity on spawning - - getRequestableSpawnersInRange() ignores height for distance +-- version history 2.0.0 - dmlZones - moved "types" to spawner - baseName defaults to zone name, as it is safe for naming diff --git a/modules/stopGaps standalone.lua b/modules/stopGaps standalone.lua index 80a5ed5..75996dc 100644 --- a/modules/stopGaps standalone.lua +++ b/modules/stopGaps standalone.lua @@ -324,6 +324,9 @@ function stopGap.update() busy = true -- count up for auto-release after n seconds trigger.action.setUserFlag(theGroup.sgName, sgState + 1) + if stopGap.verbose then + trigger.action.outText("+++StopG: [cooldown] cooldown for group <" .. name .. ">, val now is <" .. sgState .. ">.", 30) + end end if busy then @@ -331,6 +334,9 @@ function stopGap.update() else local theStaticGroup = stopGap.createStandInsForMXGroup(theGroup) stopGap.standInGroups[name] = theStaticGroup + if stopGap.verbose then + trigger.action.outText("+++StopG: [server command] placing static stand-in for group <" .. name .. ">.", 30) + end end else -- plane is currently static and visible diff --git a/modules/stopGaps.lua b/modules/stopGaps.lua index 3c5b762..cee033e 100644 --- a/modules/stopGaps.lua +++ b/modules/stopGaps.lua @@ -52,7 +52,7 @@ stopGap.requiredLibs = { --]]-- -stopGap.standInGroups = {} +stopGap.standInGroups = {} -- idx by name, if set has a static stopGap.myGroups = {} -- for fast look-up of mx orig data stopGap.stopGapZones = {} -- DML only diff --git a/modules/theDebugger.lua b/modules/theDebugger.lua index 2c9a8cb..ee8e0ce 100644 --- a/modules/theDebugger.lua +++ b/modules/theDebugger.lua @@ -12,24 +12,27 @@ debugger.log = "" --[[-- Version History - 1.0.0 - Initial version - 1.0.1 - made ups available to config zone - - changed 'on' to 'active' in config zone - - merged debugger and debugDemon - - QoL check for 'debug' attribute (no '?') - 1.1.0 - logging - - trigger.action --> debugger for outText - - persistence of logs - - save - 1.1.1 - warning when trying to set a flag to a non-int - 1.1.2 - remove command 2.0.0 - dmlZones OOP - eventmon command - - all, off, event # + - eventmon all, off, event # - standard events - - adding events + - adding events via # - events? attribute from any zone - + - eventmon last command + - q - query MSE Lua variables + - w - write/overwrite MSE Lua variables + - a - analyse Lua tables / variables + - smoke + - spawn system with predefines + - spawn coalition + - spawn number + - spawn heading + - spawn types + - spawn aircraft: add waypoints + - spawn "?" + - debuggerSpawnTypes zone + - reading debuggerSpawnTypes + - removed some silly bugs / inconsistencies --]]-- debugger.requiredLibs = { @@ -45,6 +48,7 @@ debugger.debugUnits = {} debugger.debugGroups = {} debugger.debugObjects = {} debugger.showEvents = {} +debugger.lastEvent = nil debugDemon.eventList = { ["0"] = "S_EVENT_INVALID = 0", @@ -106,6 +110,26 @@ debugDemon.eventList = { ["56"] = "S_EVENT_POSTPONED_LAND = 56", ["57"] = "S_EVENT_MAX = 57", } + +debugger.spawnTypes = { + ["inf"] = "Soldier M4", + ["ifv"] = "BTR-80", + ["tank"] = "T-90", + ["ship"] = "PERRY", + ["helo"] = "AH-1W", + ["jet"] = "MiG-21Bis", + ["awacs"] = "A-50", + ["ww2"] = "SpitfireLFMkIX", + ["bomber"] = "B-52H", + ["cargo"] = "ammo_cargo", + ["sam"] = "Roland ADS", + ["aaa"] = "ZSU-23-4 Shilka", + ["arty"] = "M-109", + ["truck"] = "KAMAZ Truck", + ["drone"] = "MQ-9 Reaper", + ["manpad"] = "Soldier stinger", + ["obj"] = "house2arm" +} -- -- Logging & saving -- @@ -235,7 +259,6 @@ function debugger.createEventMonWithZone(theZone) debugger.outText("*** monitoring events defined in <" .. theZone.name .. ">:", 30) end for idx, aFlag in pairs(flagArray) do - local evt = tonumber(aFlag) if evt and (debugger.verbose or theZone.verbose) then if evt < 0 then evt = 0 end @@ -288,7 +311,6 @@ function debugger.isObserving(flagName) end end end - return observers end @@ -476,43 +498,62 @@ end function debugger.readConfigZone() local theZone = cfxZones.getZoneByName("debuggerConfig") if not theZone then - if debugger.verbose then - debugger.outText("+++debug: NO config zone!", 30) - end theZone = cfxZones.createSimpleZone("debuggerConfig") end debugger.configZone = theZone - debugger.active = cfxZones.getBoolFromZoneProperty(theZone, "active", true) - debugger.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + debugger.active = theZone:getBoolFromZoneProperty("active", true) + debugger.verbose = theZone.verbose - if cfxZones.hasProperty(theZone, "on?") then - debugger.onFlag = cfxZones.getStringFromZoneProperty(theZone, "on?", "") + if theZone:hasProperty("on?") then + debugger.onFlag = theZone:getStringFromZoneProperty("on?", "") debugger.lastOn = cfxZones.getFlagValue(debugger.onFlag, theZone) end - if cfxZones.hasProperty(theZone, "off?") then - debugger.offFlag = cfxZones.getStringFromZoneProperty(theZone, "off?", "") + if theZone:hasProperty("off?") then + debugger.offFlag = theZone:getStringFromZoneProperty("off?", "") debugger.lastOff = cfxZones.getFlagValue(debugger.offFlag, theZone) end - if cfxZones.hasProperty(theZone, "reset?") then - debugger.resetFlag = cfxZones.getStringFromZoneProperty(theZone, "reset?", "") + if theZone:hasProperty("reset?") then + debugger.resetFlag = theZone:getStringFromZoneProperty("reset?", "") debugger.lastReset = cfxZones.getFlagValue(debugger.resetFlag, theZone) end - if cfxZones.hasProperty(theZone, "state?") then - debugger.stateFlag = cfxZones.getStringFromZoneProperty(theZone, "state?", "") + if theZone:hasProperty("state?") then + debugger.stateFlag = theZone:getStringFromZoneProperty("state?", "") debugger.lastState = cfxZones.getFlagValue(debugger.stateFlag, theZone) end - debugger.ups = cfxZones.getNumberFromZoneProperty(theZone, "ups", 4) - - if debugger.verbose then - debugger.outText("+++debug: read config", 30) - end + debugger.ups = theZone:getNumberFromZoneProperty("ups", 4) end +function debugger.readSpawnTypeZone() + local theZone = cfxZones.getZoneByName("debuggerSpawnTypes") + if not theZone then + theZone = cfxZones.createSimpleZone("debuggerSpawnTypes") + end + local allAttribuites = theZone:getAllZoneProperties() + for attrName, aValue in pairs(allAttribuites) do + local theLow = string.lower(attrName) + local before = debugger.spawnTypes[theLow] + if before then + debugger.spawnTypes[theLow] = aValue + if theZone.verbose or debugger.verbose then + trigger.action.outText("+++debug: changed generic '" .. theLow .. "' from <" .. before .. "> to <" .. aValue .. ">", 30) + end + else + if theZone.verbose or debugger.verbose then + if theLow == "verbose" then -- filtered + else + trigger.action.outText("+++debug: generic '" .. theLow .. "' unknown, not replaced.", 30) + end + end + end + end +end + + function debugger.start() -- lib check if not dcsCommon.libCheck then @@ -526,6 +567,9 @@ function debugger.start() -- read config debugger.readConfigZone() + -- read spawn types + debugger.readSpawnTypeZone() + -- process debugger Zones -- old style local attrZones = cfxZones.getZonesWithAttributeNamed("debug?") @@ -536,7 +580,7 @@ function debugger.start() local attrZones = cfxZones.getZonesWithAttributeNamed("debug") for k, aZone in pairs(attrZones) do - debugger.outText("***Warning: Zone <" .. aZone.name .. "> has a 'debug' flag. Are you perhaps missing a '?'", 30) + debugger.outText("***Warning: Zone <" .. aZone.name .. "> has a 'debug' attribute. Are you perhaps missing a '?'", 30) end local attrZones = cfxZones.getZonesWithAttributeNamed("events?") @@ -546,7 +590,7 @@ function debugger.start() local attrZones = cfxZones.getZonesWithAttributeNamed("events") for k, aZone in pairs(attrZones) do - debugger.outText("***Warning: Zone <" .. aZone.name .. "> has a 'debug' flag. Are you perhaps missing a '?'", 30) + debugger.outText("***Warning: Zone <" .. aZone.name .. "> has an 'events' attribute. Are you perhaps missing a '?'", 30) end -- events @@ -683,7 +727,7 @@ function debugDemon.getArgs(theCommands) end -- --- stage demon's main command interpreter. +-- debug demon's main command interpreter. -- magic lies in using the keywords as keys into a -- function table that holds all processing functions -- I wish we had that back in the Oberon days. @@ -743,7 +787,7 @@ debugger.outText("*** debugger: commands are:" .. "\n " .. debugDemon.markOfDemon .. "o [with ] -- observe a flag for change" .. "\n " .. debugDemon.markOfDemon .. "forget [with ] -- stop observing a flag" .. "\n " .. debugDemon.markOfDemon .. "new [[for] ] -- create observer for flags" .. - "\n " .. debugDemon.markOfDemon .. "update [[to] ] -- change observer's condition" .. + "\n " .. debugDemon.markOfDemon .. "update [to] -- change observer's condition" .. "\n " .. debugDemon.markOfDemon .. "drop -- remove observer from debugger" .. "\n " .. debugDemon.markOfDemon .. "list [] -- list observers [name contains ]" .. "\n " .. debugDemon.markOfDemon .. "who -- all who observe " .. @@ -752,10 +796,16 @@ debugger.outText("*** debugger: commands are:" .. "\n\n " .. debugDemon.markOfDemon .. "snap [] -- create new snapshot of flags" .. "\n " .. debugDemon.markOfDemon .. "compare -- compare snapshot flag values with current" .. "\n " .. debugDemon.markOfDemon .. "note -- add to the text log" .. - "\n\n " .. debugDemon.markOfDemon .. "remove -- remove named item from mission" .. + "\n\n " .. debugDemon.markOfDemon .. "spawn [] [] [heading=] | [?] -- spawn" .. + "\n units/aircraft/objects (? for help)" .. + "\n " .. debugDemon.markOfDemon .. "remove -- remove named item from mission" .. + "\n " .. debugDemon.markOfDemon .. "smoke -- place colored smoke on the ground" .. + "\n " .. debugDemon.markOfDemon .. "boom -- place explosion of strenght on the ground" .. "\n\n " .. debugDemon.markOfDemon .. "eventmon [all | off | | ?] -- show events for all | none | event | list" .. + "\n " .. debugDemon.markOfDemon .. "eventmon last -- analyse last reported event" .. "\n\n " .. debugDemon.markOfDemon .. "q -- Query value of Lua variable " .. + "\n " .. debugDemon.markOfDemon .. "a -- Analyse structure of Lua variable " .. "\n " .. debugDemon.markOfDemon .. "w [=] -- Write to variable " .. "\n\n " .. debugDemon.markOfDemon .. "start -- starts debugger" .. "\n " .. debugDemon.markOfDemon .. "stop -- stop debugger" .. @@ -807,7 +857,7 @@ function debugDemon.processNewCommand(args, event) return false end theZone.debugInputMethod = condition - end + end debugger.addDebugger(theZone) debugger.outText("*** [" .. dcsCommon.nowString() .. "] debugger: new observer <" .. observerName .. "> for <" .. theZone.debugInputMethod .. ">", 30) @@ -815,7 +865,7 @@ function debugDemon.processNewCommand(args, event) end function debugDemon.processUpdateCommand(args, event) - -- syntax update [[to] ] + -- syntax update [to] local observerName = args[1] if not observerName then debugger.outText("*** update: missing observer name.", 30) @@ -1271,6 +1321,7 @@ end function debugDemon.doEventMon(theEvent) if not theEvent then return end + if not debugger.active then return end local ID = theEvent.id if debugger.showEvents[ID] then -- we show this event @@ -1287,11 +1338,47 @@ function debugDemon.doEventMon(theEvent) end end debugger.outText(m, 30) + -- save it to lastevent so we can analyse + debugger.lastEvent = theEvent end end +debugDemon.m = "" +-- dumpVar2m, invoke externally dumpVar2m(varname, var) +function debugDemon.dumpVar2m(key, value, prefix, inrecursion) + -- based on common's dumpVar, appends to var "m" + if not inrecursion then + -- start, init m + debugDemon.m = "analysis of <" .. key .. ">\n===" + end + if not value then value = "nil" end + if not prefix then prefix = "" end + prefix = " " .. prefix + if type(value) == "table" then + debugDemon.m = debugDemon.m .. "\n" .. prefix .. key .. ": [ " + -- iterate through all kvp + for k,v in pairs (value) do + debugDemon.dumpVar2m(k, v, prefix, true) + end + debugDemon.m = debugDemon.m .. "\n" .. prefix .. " ] - end " .. key + + elseif type(value) == "boolean" then + local b = "false" + if value then b = "true" end + debugDemon.m = debugDemon.m .. "\n" .. prefix .. key .. ": " .. b + + else -- simple var, show contents, ends recursion + debugDemon.m = debugDemon.m .. "\n" .. prefix .. key .. ": " .. value + end + + if not inrecursion then + -- output a marker to find in the log / screen + debugDemon.m = debugDemon.m .. "\n" .. "=== analysis end\n" + end +end + function debugDemon.processEventMonCommand(args, event) - -- turn event monito on/off + -- turn event monitor all/off/?/last -- syntax: -eventmon on|off local aParam = dcsCommon.trim(event.remainder) if not aParam or aParam:len() < 1 then @@ -1300,7 +1387,6 @@ function debugDemon.processEventMonCommand(args, event) aParam = string.upper(aParam) evtNum = tonumber(aParam) if aParam == "ON" or aParam == "ALL" then --- debugger.eventmon = true debugger.outText("*** eventmon: turned ON, showing ALL events", 30) local events = {} for idx,evt in pairs(debugDemon.eventList) do @@ -1322,6 +1408,13 @@ function debugDemon.processEventMonCommand(args, event) m = m .. "\n" .. evt end debugger.outText(m .. "\n*** end of list", 30) + elseif aParam == "LAST" then + if debugger.lastEvent then + debugDemon.dumpVar2m("event", debugger.lastEvent) + debugger.outText(debugDemon.m, 39) + else + debugger.outText("*** eventmon: no event on record", 39) + end else debugger.outText("*** eventmon: unknown parameter <" .. event.remainder .. ">", 30) end @@ -1335,9 +1428,7 @@ end function debugDemon.processQueryCommand(args, event) -- syntax -q with name a (qualified) Lua table reference local theName = args[1] --- local p = args [2] --- trigger.action.outText("args1 = " .. theName, 30) --- if args[2] then trigger.action.outText("param = " .. args[2], 30) end + if not theName then debugger.outText("*** q: missing Lua table/element name.", 30) return false -- allows correction @@ -1369,6 +1460,33 @@ function debugDemon.processQueryCommand(args, event) return true end +function debugDemon.processAnalyzeCommand(args, event) + -- syntax -a with name a (qualified) Lua table reference + local theName = args[1] + + if not theName then + debugger.outText("*** a: missing Lua table/element name.", 30) + return false -- allows correction + end + theName = dcsCommon.stringRemainsStartingWith(event.remainder, theName) + + -- put this into a string, and execute it + local exec = "return " .. theName + local f = loadstring(exec) + local res + if pcall(f) then + res = f() + debugDemon.dumpVar2m(theName, res) + res = debugDemon.m + else + res = "[Lua error]" + end + + debugger.outText("[" .. dcsCommon.nowString() .. "] <" .. theName .. "> = ".. res, 30) + + return true +end + function debugDemon.processWriteCommand(args, event) -- syntax -w with name a (qualified) Lua table reference and value a Lua value (including strings, with quotes of course). {} means an empty set etc. you CAN call into DCS MSE with this, and create a lot of havoc. -- also, allow "=" semantic, -w p = {x=1, y=2} @@ -1402,6 +1520,324 @@ function debugDemon.processWriteCommand(args, event) return true end +-- +-- smoke & boom +-- + +function debugDemon.processSmokeCommand(args, event) + -- syntax -color + local color = 0 -- green default + local colorCom = args[1] + if colorCom then + colorCom = colorCom:lower() + if colorCom == "red" or colorCom == "1" then color = 1 + elseif colorCom == "white" or colorCom == "2" then color = 2 + elseif colorCom == "orange" or colorCom == "3" then color = 3 + elseif colorCom == "blue" or colorCom == "4" then color = 4 + elseif colorCom == "green" or colorCom == "0" then color = 0 + else + debugger.outText("*** smoke: unknown color <" .. colorCom .. ">, using green.", 30) + end + local pos = event.pos + local h = land.getHeight({x = pos.x, y = pos.z}) + 1 + local p = { x = event.pos.x, y = h, z = event.pos.z} + trigger.action.smoke(p, color) + debugger.outText("*** smoke: placed smoke at <" .. dcsCommon.point2text(p, true) .. ">.", 30) + end +end + +function debugDemon.processBoomCommand(args, event) + -- syntax -color + local power = 1 -- boom default + local powerCom = args[1] + if powerCom then + powerCom = tonumber(powerCom) + if powerCom then + power = powerCom + end + end + local pos = event.pos + local h = land.getHeight({x = pos.x, y = pos.z}) + 1 + local p = { x = event.pos.x, y = h, z = event.pos.z} + trigger.action.explosion(p, power) + debugger.outText("*** boom: placed <" .. power .. "> explosion at <" .. dcsCommon.point2text(p, true) .. ">.", 30) +end + +-- +-- spawning units at the location of the mark +-- + +function debugDemon.getCoaFromCommand(args) + for i=1, #args do + local aParam = args[i] + if dcsCommon.stringStartsWith(aParam, "red", true) then return 1, i end + if dcsCommon.stringStartsWith(aParam, "blu", true) then return 2, i end + if dcsCommon.stringStartsWith(aParam, "neu", true) then return 0, i end + end + return 0, nil +end + +function debugDemon.getAirFromCommand(args) + for i=1, #args do + local aParam = args[i] + if aParam:lower() == "inair" then return true, i end + if aParam:lower() == "air" then return true, i end + end + return false, nil +end + +function debugDemon.getHeadingFromCommand(args) + for i=1, #args do + local aParam = args[i] + if dcsCommon.stringStartsWith(aParam, "heading=", true) then + local parts = dcsCommon.splitString(aParam, "=") + local num = parts[2] + if num and tonumber(num) then + return tonumber(num), i + end + end + end + return 0, nil +end + +function debugDemon.getNumFromCommand(args) + for i=1, #args do + local aParam = args[i] + local num = tonumber(aParam) + if num then return num, i end + end + return 1, nil +end + +function debugDemon.processSpawnCommand(args, event) + -- complex syntax: + -- spawn [red|blue|neutral] [number] [heading=] | "?" + local params = dcsCommon.clone(args) +-- for i=1, #params do +-- trigger.action.outText("arg[" .. i .."] = <" .. params[i] .. ">", 30) +-- end + + -- get coalition from input + + local coa, idx = debugDemon.getCoaFromCommand(params) + if idx then table.remove(params, idx) end + local inAir, idy = debugDemon.getAirFromCommand(params) + if idy then table.remove(params, idy) end + local num, idz = debugDemon.getNumFromCommand(params) + if idz then table.remove(params, idz) end + local heading, idk = debugDemon.getHeadingFromCommand(params) + if idk then table.remove(params, idk) end + + local class = params[1] + if not class then + debugger.outText("*** spawn: missing keyword (what to spawn).", 30) + return + end + + class = class:lower() + + -- when we are here, we have reduced all params, so class is [1] +-- trigger.action.outText("spawn with class <" .. class .. ">, num <" .. num .. ">, inAir <" .. dcsCommon.bool2Text(inAir) .. ">, coa <" .. coa .. ">, hdg <" .. heading .. ">", 30) + heading = heading * 0.0174533 -- in rad + + local pos = event.pos + local h = land.getHeight({x = pos.x, y = pos.z}) + 1 + local p = { x = event.pos.x, y = h, z = event.pos.z} + + if class == "tank" or class == "tanks" then + -- spawn the 'tank' class + local theType = debugger.spawnTypes["tank"] + return debugDemon.spawnTypeWithCat(theType, coa, num, p, nil,heading) + elseif class == "man" or class == "soldier" or class == "men" then + local theType = debugger.spawnTypes["inf"] + return debugDemon.spawnTypeWithCat(theType, coa, num, p, nil,heading) + + elseif class == "inf" or class == "ifv" or class == "sam" or + class == "arty" or class == "aaa" then + local theType = debugger.spawnTypes[class] + return debugDemon.spawnTypeWithCat(theType, coa, num, p, nil,heading) + elseif class == "truck" or class == "trucks" then + local theType = debugger.spawnTypes["truck"] + return debugDemon.spawnTypeWithCat(theType, coa, num, p, nil,heading) + elseif class == "manpad" or class == "manpads" or class == "pad" or class == "pads" then + local theType = debugger.spawnTypes["manpad"] + return debugDemon.spawnTypeWithCat(theType, coa, num, p, nil,heading) + + elseif class == "ship" or class == "ships" then + local theType = debugger.spawnTypes["ship"] + return debugDemon.spawnTypeWithCat(theType, coa, num, p, Group.Category.SHIP, heading) + + elseif class == "jet" or class == "jets" then + local theType = debugger.spawnTypes["jet"] + return debugDemon.spawnAirWIthCat(theType, coa, num, p, nil, 1000, 160, heading) + + elseif class == "ww2" then + local theType = debugger.spawnTypes[class] + return debugDemon.spawnAirWIthCat(theType, coa, num, p, nil, 1000, 100, heading) + + elseif class == "bomber" or class == "awacs" then + local theType = debugger.spawnTypes[class] + return debugDemon.spawnAirWIthCat(theType, coa, num, p, nil, 8000, 200, heading) + + elseif class == "drone" then + local theType = debugger.spawnTypes[class] + return debugDemon.spawnAirWIthCat(theType, coa, num, p, nil, 3000, 77, heading) + + elseif class == "helo" or class == "helos" then + local theType = debugger.spawnTypes["helo"] + return debugDemon.spawnAirWIthCat(theType, coa, num, p, Group.Category.HELICOPTER, 200, 40, heading) + + elseif class == "cargo" or class == "obj" then + local isCargo = (class == "cargo") + local theType = debugger.spawnTypes[class] + return debugDemon.spawnObjects(theType, coa, num, p, isCargo, heading) + + elseif class == "?" then + local m = " spawn: invoke '-spawn [number] [coalition] [heading]' with \n" .. + " number = any number, default is 1\n" .. + " coalition = 'red' | 'blue' | 'neutral', default is neutral\n" .. + " heading = 'heading=' - direction to face, in degrees, no blanks\n" .. + " = what to spawn, any of the following pre-defined (no quotes)\n" .. + " 'tank' - a tank " .. debugDemon.tellType("tank") .. "\n" .. + " 'ifv' - an IFV " .. debugDemon.tellType("ifv") .. "\n" .. + " 'inf' - an infantry soldier " .. debugDemon.tellType("inf") .. "\n" .. + " 'sam' - a SAM vehicle " .. debugDemon.tellType("sam") .. "\n" .. + " 'aaa' - a AAA vehicle " .. debugDemon.tellType("aaa") .. "\n" .. + " 'arty' - artillery vehicle " .. debugDemon.tellType("arty") .. "\n" .. + " 'manpad' - a soldier with SAM " .. debugDemon.tellType("manpad") .. "\n" .. + " 'truck' - a truck " .. debugDemon.tellType("truck") .. "\n\n" .. + " 'jet' - a fast aircraft " .. debugDemon.tellType("jet") .. "\n" .. + " 'ww2' - a warbird " .. debugDemon.tellType("ww2") .. "\n" .. + " 'bomber' - a heavy bomber " .. debugDemon.tellType("bomber") .. "\n" .. + " 'awacs' - an AWACS plane " .. debugDemon.tellType("awacs") .. "\n" .. + " 'drone' - a drone " .. debugDemon.tellType("drone") .. "\n" .. + " 'helo' - a helicopter " .. debugDemon.tellType("helo") .. "\n\n" .. + " 'ship' - a naval unit" .. debugDemon.tellType("ship") .. "\n\n" .. + " 'cargo' - some helicopter cargo " .. debugDemon.tellType("cargo") .. "\n" .. + " 'obj' - a static object " .. debugDemon.tellType("obj") .. "\n" + + debugger.outText(m, 30) + return true + else + debugger.outText("*** spawn: unknown kind <" .. class .. ">.", 30) + return false + end +end + +function debugDemon.tellType(theType) + return " [" .. debugger.spawnTypes[theType] .. "]" +end + +function debugDemon.spawnTypeWithCat(theType, coa, num, p, cat, heading) + trigger.action.outText("heading is <" .. heading .. ">", 30) + if not cat then cat = Group.Category.GROUND end + if not heading then heading = 0 end + + local xOff = 0 + local yOff = 0 + -- build group + local groupName = dcsCommon.uuid(theType) + local gData = dcsCommon.createEmptyGroundGroupData(groupName) + for i=1, num do + local aUnit = {} + aUnit = dcsCommon.createGroundUnitData(groupName .. "-" .. i, theType) + --aUnit.heading = heading + dcsCommon.addUnitToGroupData(aUnit, gData, xOff, yOff, heading) + xOff = xOff + 10 + yOff = yOff + 10 + end + + -- arrange in a grid formation + local radius = math.floor(math.sqrt(num) * 10) + if cat == Group.Category.SHIP then + radius = math.floor(math.sqrt(num) * 100) + end + + dcsCommon.arrangeGroupDataIntoFormation(gData, radius, 10, "GRID") + + -- move to destination + dcsCommon.moveGroupDataTo(gData, p.x, p.z) + + -- spawn + local cty = dcsCommon.getACountryForCoalition(coa) + local theGroup = coalition.addGroup(cty, cat, gData) + if theGroup then + debugger.outText("[" .. dcsCommon.nowString() .. "] created units at " .. dcsCommon.point2text(p, true), 30) + return true + else + debugger.outText("[" .. dcsCommon.nowString() .. "] failed to created units", 30) + return false + end + return false +end + +function debugDemon.spawnAirWIthCat(theType, coa, num, p, cat, alt, speed, heading) + if not cat then cat = Group.Category.AIRPLANE end + local xOff = 0 + local yOff = 0 + -- build group + local groupName = dcsCommon.uuid(theType) + local gData = dcsCommon.createEmptyAircraftGroupData(groupName) + for i=1, num do + local aUnit = {} + aUnit = dcsCommon.createAircraftUnitData(groupName .. "-" .. i, theType, false, alt, speed) + --aUnit.heading = heading + dcsCommon.addUnitToGroupData(aUnit, gData, xOff, yOff, heading) + xOff = xOff + 30 + yOff = yOff + 30 + end + -- move to destination + dcsCommon.moveGroupDataTo(gData, p.x, p.z) + + -- make waypoints: initial point and 200 km away in direction heading + local p2 = dcsCommon.pointInDirectionOfPointXYY(heading, 200000, p) + local wp1 = dcsCommon.createSimpleRoutePointData(p, alt, speed) + local wp2 = dcsCommon.createSimpleRoutePointData(p2, alt, speed) + -- add waypoints + dcsCommon.addRoutePointForGroupData(gData, wp1) + dcsCommon.addRoutePointForGroupData(gData, wp2) + + -- spawn + local cty = dcsCommon.getACountryForCoalition(coa) + local theGroup = coalition.addGroup(cty, cat, gData) + if theGroup then + debugger.outText("[" .. dcsCommon.nowString() .. "] created air units at " .. dcsCommon.point2text(p, true), 30) + return true + else + debugger.outText("[" .. dcsCommon.nowString() .. "] failed to created air units", 30) + return false + end +end + +function debugDemon.spawnObjects(theType, coa, num, p, cargo, heading) + if not cargo then cargo = false end + local cty = dcsCommon.getACountryForCoalition(coa) + local xOff = 0 + local yOff = 0 + local success = false + -- build static objects and spawn individually + for i=1, num do + local groupName = dcsCommon.uuid(theType) + local gData = dcsCommon.createStaticObjectData(groupName, theType, 0, false, cargo, 1000) + gData.x = xOff + p.x + gData.y = yOff + p.z + gData.heading = heading + local theGroup = coalition.addStaticObject(cty, gData) + success = theGroup + xOff = xOff + 10 -- stagger by 10m, 10m + yOff = yOff + 10 + end + + -- was it worth it? + if success then + debugger.outText("[" .. dcsCommon.nowString() .. "] created objects at " .. dcsCommon.point2text(p, true), 30) + return true + else + debugger.outText("[" .. dcsCommon.nowString() .. "] failed to create objects", 30) + return false + end +end + -- -- init and start @@ -1430,6 +1866,7 @@ function debugDemon.readConfigZone() end end + function debugDemon.init() if not dcsCommon.libCheck then trigger.action.outText("cfx interactive debugger requires dcsCommon", 30) @@ -1473,10 +1910,15 @@ function debugDemon.init() debugDemon.addCommndProcessor("help", debugDemon.processHelpCommand) debugDemon.addCommndProcessor("remove", debugDemon.processRemoveCommand) + debugDemon.addCommndProcessor("spawn", debugDemon.processSpawnCommand) + debugDemon.addCommndProcessor("add", debugDemon.processSpawnCommand) debugDemon.addCommndProcessor("eventmon", debugDemon.processEventMonCommand) debugDemon.addCommndProcessor("q", debugDemon.processQueryCommand) debugDemon.addCommndProcessor("w", debugDemon.processWriteCommand) + debugDemon.addCommndProcessor("a", debugDemon.processAnalyzeCommand) + debugDemon.addCommndProcessor("smoke", debugDemon.processSmokeCommand) + debugDemon.addCommndProcessor("boom", debugDemon.processBoomCommand) return true end @@ -1512,14 +1954,9 @@ end - inspect objects, dumping category, life, if it's tasking, latLon, alt, speed, direction - exec files. save all commands and then run them from script - - - query objects: -q persistence.active returns boolean, true - -q x.y returns table, 12 elements - -q a.b.x returns number 12 - -q d.e.f returns string "asdasda..." - -q sada returs - xref: which zones/attributes reference a flag, g.g. '-xref go' + + - track lua vars for change in value - - dml version can config to start with events list, e.g. 1, 4, 7 --]]-- diff --git a/modules/unitPersistence.lua b/modules/unitPersistence.lua index cbb3d2d..7517498 100644 --- a/modules/unitPersistence.lua +++ b/modules/unitPersistence.lua @@ -1,10 +1,10 @@ unitPersistence = {} -unitPersistence.version = '1.1.1' +unitPersistence.version = '2.0.0' unitPersistence.verbose = false unitPersistence.updateTime = 60 -- seconds. Once every minute check statics unitPersistence.requiredLibs = { - "dcsCommon", -- always - "cfxZones", -- Zones, of course + "dcsCommon", + "cfxZones", "persistence", "cfxMX", } @@ -19,7 +19,8 @@ unitPersistence.requiredLibs = { 1.1.0 - added air and sea units - for filtering destroyed units 1.1.1 - fixed static link (again) - fixed air spawn (fixed wing) - + 2.0.0 - dmlZones, OOP + cleanup REQUIRES PERSISTENCE AND MX diff --git a/modules/williePete.lua b/modules/williePete.lua index 8f590bc..3c05c2f 100644 --- a/modules/williePete.lua +++ b/modules/williePete.lua @@ -1,5 +1,5 @@ williePete = {} -williePete.version = "1.0.2" +williePete.version = "2.0.0" williePete.ups = 10 -- we update at 10 fps, so accuracy of a -- missile moving at Mach 2 is within 33 meters, -- with interpolation even at 3 meters @@ -14,12 +14,16 @@ williePete.requiredLibs = { 1.0.0 - Initial version 1.0.1 - update to suppress verbosity 1.0.2 - added Gazelle WP - + 2.0.0 - dmlZones, OOP + - Guards for multi-unit player groups + - getFirstLivingPlayerInGroupNamed() --]]-- williePete.willies = {} williePete.wpZones = {} williePete.playerGUIs = {} -- used for unit guis +williePete.groupGUIs = {} -- because some people may want to install +-- multip-unit player groups williePete.blastedObjects = {} -- used when we detonate something -- recognizes WP munitions. May require regular update when new @@ -90,30 +94,29 @@ end function williePete.createWPZone(aZone) - aZone.coalition = cfxZones.getCoalitionFromZoneProperty(aZone, "wpTarget", 0) -- side that marks it on map, and who fires arty - aZone.shellStrength = cfxZones.getNumberFromZoneProperty(aZone, "shellStrength", 500) -- power of shells (strength) - aZone.shellNum = cfxZones.getNumberFromZoneProperty(aZone, "shellNum", 17) -- number of shells in bombardment - aZone.transitionTime = cfxZones.getNumberFromZoneProperty(aZone, "transitionTime", 20) -- average time of travel for projectiles - aZone.coolDown = cfxZones.getNumberFromZoneProperty(aZone, "coolDown", 180) -- cooldown after arty fire, used to set readyTime - aZone.baseAccuracy = cfxZones.getNumberFromZoneProperty(aZone, "baseAccuracy", 50) + aZone.coalition = aZone:getCoalitionFromZoneProperty("wpTarget", 0) -- side that marks it on map, and who fires arty + aZone.shellStrength = aZone:getNumberFromZoneProperty( "shellStrength", 500) -- power of shells (strength) + aZone.shellNum = aZone:getNumberFromZoneProperty("shellNum", 17) -- number of shells in bombardment + aZone.transitionTime = aZone:getNumberFromZoneProperty( "transitionTime", 20) -- average time of travel for projectiles + aZone.coolDown = aZone:getNumberFromZoneProperty("coolDown", 180) -- cooldown after arty fire, used to set readyTime + aZone.baseAccuracy = aZone:getNumberFromZoneProperty( "baseAccuracy", 50) 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.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, "method") then - aZone.wpMethod = cfxZones.getStringFromZoneProperty(aZone, "method", "change") + aZone.wpMethod = aZone:getStringFromZoneProperty("wpMethod", "change") + if aZone:hasProperty("method") then + aZone.wpMethod = aZone:getStringFromZoneProperty("method", "change") end - if cfxZones.hasProperty(aZone, "wpFire!") then - aZone.wpFire = cfxZones.getStringFromZoneProperty(aZone, "wpFire!", " in group <" .. gName .. ">. Skipped.", 30) + elseif williePete.groupGUIs[gName] then + trigger.action.outText("+++WP: Warning: POSSIBLE MULTI-PLAYER UNIT GROUP DETECTED. We already have WP menu for Player Group <" .. gName .. ">. Skipped, only first unit supported. ", 30) + else + unitInfo.root = missionCommands.addSubMenuForGroup(unitInfo.gID, "FAC") + unitInfo.checkIn = missionCommands.addCommandForGroup(unitInfo.gID, "Check In", unitInfo.root, williePete.redirectCheckIn, unitInfo) + williePete.groupGUIs[gName] = unitInfo + williePete.playerGUIs[gName] = unitInfo + end end - -- store it - williePete.playerGUIs[uName] = unitInfo + -- store it - WARNING: ASSUMES SINGLE-UNIT Player Groups + --williePete.playerGUIs[uName] = unitInfo end end @@ -177,7 +189,7 @@ function williePete.doBoom(args) -- note that unit who commânded fire may no longer be alive -- so check it every time. unit must be alive -- to receive credits later - local uName = unitInfo.name + local uName = unitInfo.gName local blastRad = math.floor(math.sqrt(args.strength)) * 2 if blastRad < 10 then blastRad = 10 end @@ -187,7 +199,7 @@ function williePete.doBoom(args) if williePete.verbose then trigger.action.outText("<" .. aName .. "> is in blast Radius (" .. blastRad .. "m) of shells for <" .. uName .. ">'s target coords", 30) end - williePete.blastedObjects[aName] = uName -- last one gets the kill + williePete.blastedObjects[aName] = unitInfo.name -- last one gets the kill end end trigger.action.explosion(args.point, args.strength) @@ -237,16 +249,36 @@ function williePete.redirectCheckIn(unitInfo) timer.scheduleFunction(williePete.doCheckIn, unitInfo, timer.getTime() + 0.1) end +-- fix for multi-unit player groups where only one of them is +-- alive: get first living player in group. Will be added to +-- dcsCommon soon +function williePete.getFirstLivingPlayerInGroupNamed(gName) + local theGroup = Group.getByName(gName) + if not theGroup then return nil end + local theUnits = theGroup:getUnits() + for idx, aUnit in pairs(theUnits) do + if Unit.isExist(aUnit) and aUnit.getPlayerName and + aUnit:getPlayerName() then + return aUnit -- return first living player unit + end + end + return nil +end + function williePete.doCheckIn(unitInfo) - --trigger.action.outText("check-in received", 30) - local theUnit = Unit.getByName(unitInfo.name) + -- WARNING: unitInfo points to first processed player in + -- group. May not work fully with multi-unit player groups + local gName = unitInfo.gName + local theUnit = williePete.getFirstLivingPlayerInGroupNamed(gName) --Unit.getByName(unitInfo.name) if not theUnit then -- dead man calling. Pilot dead but unit still alive + -- OR second unit in multiplayer group, but unit 1 + -- does not / no longer exists trigger.action.outText("Calling station, say again, can't read you.", 30) return end - local p = theUnit:getPoint() + local p = theUnit:getPoint() -- only react to first player unit local theZone, dist = williePete.closestCheckInTgtZoneForCoa(p, unitInfo.coa) if not theZone then @@ -256,21 +288,23 @@ function williePete.doCheckIn(unitInfo) trigger.action.outSoundForGroup(unitInfo.gID, williePete.guiSound) return end - - trigger.action.outTextForGroup(unitInfo.gID, "Too far from target zone, closest target zone is " .. theZone.name, 30) + dist = math.floor(dist /100) / 10 + bearing = dcsCommon.bearingInDegreesFromAtoB(p, theZone:getPoint()) + trigger.action.outTextForGroup(unitInfo.gID, unitInfo.gName .. ", you are too far from target zone, closest target zone is " .. theZone.name .. ", " .. dist .. "km at bearing " .. bearing .. "°", 30) trigger.action.outSoundForGroup(unitInfo.gID, theZone.guiSound) return end -- we are now checked in to zone -- unless we are already checked in - if theZone.checkedIn[unitInfo.name] then - trigger.action.outTextForGroup(unitInfo.gID, unitInfo.name .. ", " .. theZone.name .. ", we heard you the first time, proceed.", 30) + -- NOTE: we use group name, not unit name! + if theZone.checkedIn[unitInfo.gName] then + trigger.action.outTextForGroup(unitInfo.gID, unitInfo.gName .. ", " .. theZone.name .. ", we heard you the first time, proceed.", 30) trigger.action.outSoundForGroup(unitInfo.gID, theZone.guiSound) return end -- we now check in - theZone.checkedIn[unitInfo.name] = unitInfo + theZone.checkedIn[unitInfo.gName] = unitInfo -- add the 'Target marked' menu unitInfo.targetMarked = missionCommands.addCommandForGroup(unitInfo.gID, "Target Marked, commence firing", unitInfo.root, williePete.redirectTargetMarked, unitInfo) @@ -280,7 +314,7 @@ function williePete.doCheckIn(unitInfo) -- add 'check out' 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.outTextForGroup(unitInfo.gID, "Roger " .. unitInfo.gName .. ", " .. theZone.name .. " tracks you, standing by for target data.", 30) trigger.action.outSoundForGroup(unitInfo.gID, theZone.guiSound) end @@ -293,17 +327,17 @@ function williePete.doCheckOut(unitInfo) local wasCheckedIn = false local fromZone = "" for idx, theZone in pairs(williePete.wpZones) do - if theZone.checkedIn[unitInfo.name] then + if theZone.checkedIn[unitInfo.gName] then wasCheckedIn = true fromZone = theZone.name end - theZone.checkedIn[unitInfo.name] = nil + theZone.checkedIn[unitInfo.gName] = nil end if not wasCheckedIn then - trigger.action.outTextForGroup(unitInfo.gID, unitInfo.name .. ", roger cecked-out. Good hunting!", 30) + trigger.action.outTextForGroup(unitInfo.gID, unitInfo.gName .. ", roger cecked-out. Good hunting!", 30) trigger.action.outSoundForGroup(unitInfo.gID, williePete.guiSound) else - trigger.action.outTextForGroup(unitInfo.gID, unitInfo.name .. "has checked out of " .. fromZone ..".", 30) + trigger.action.outTextForGroup(unitInfo.gID, unitInfo.gName .. " has checked out of " .. fromZone ..".", 30) trigger.action.outSoundForGroup(unitInfo.gID, williePete.guiSound) end @@ -326,7 +360,7 @@ function williePete.rogerDodger(args) local unitInfo = args[1] local theZone = args[2] - trigger.action.outTextForCoalition(unitInfo.coa, "Roger " .. unitInfo.name .. ", good copy, firing.", 30) + trigger.action.outTextForCoalition(unitInfo.coa, "Roger " .. unitInfo.gName .. ", good copy, firing.", 30) trigger.action.outSoundForCoalition(unitInfo.coa, theZone.ackSound) end @@ -357,9 +391,9 @@ function williePete.doTargetMarked(unitInfo) local tgtZone = unitInfo.wpInZone -- see if we are checked into that zone - if not tgtZone.checkedIn[unitInfo.name] then + if not tgtZone.checkedIn[unitInfo.gName] then -- zones don't match - trigger.action.outTextForGroup(unitInfo.gID, "Say again " .. unitInfo.name .. ", we have crosstalk. Try and reset coms", 30) + trigger.action.outTextForGroup(unitInfo.gID, "Say again " .. unitInfo.gName .. ", we have crosstalk. Try and reset coms", 30) trigger.action.outSoundForGroup(unitInfo.gID, williePete.guiSound) return end @@ -368,7 +402,7 @@ function williePete.doTargetMarked(unitInfo) local timeRemaining = math.floor(tgtZone.readyTime - now) if timeRemaining > 0 then -- zone not ready - trigger.action.outTextForGroup(unitInfo.gID, "Stand by " .. unitInfo.name .. ", artillery not ready. Expect " .. timeRemaining + math.random(1, 5) .. " seconds.", 30) + trigger.action.outTextForGroup(unitInfo.gID, "Stand by " .. unitInfo.gName .. ", artillery not ready. Expect " .. timeRemaining + math.random(1, 5) .. " seconds.", 30) trigger.action.outSoundForGroup(unitInfo.gID, tgtZone.guiSound) return end @@ -378,7 +412,7 @@ function williePete.doTargetMarked(unitInfo) local grid = coord.LLtoMGRS(coord.LOtoLL(unitInfo.pos)) local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing local theLoc = mgrs - trigger.action.outTextForCoalition(unitInfo.coa, tgtZone.name ..", " .. unitInfo.name .." is transmitting target location. Fire at " .. theLoc .. ", elevation " .. alt .. " meters, target marked.", 30) + trigger.action.outTextForCoalition(unitInfo.coa, tgtZone.name ..", " .. unitInfo.gName .." is transmitting target location. Fire at " .. theLoc .. ", elevation " .. alt .. " meters, target marked.", 30) trigger.action.outSoundForCoalition(unitInfo.coa, tgtZone.guiSound) timer.scheduleFunction(williePete.rogerDodger, {unitInfo, tgtZone},timer.getTime() + math.random(2, 5)) @@ -401,10 +435,13 @@ end -- return true if a zone is actively tracking theUnit to place -- a wp -function williePete.zoneIsTracking(theUnit) +function williePete.zoneIsTracking(theUnit) -- group level! local uName = theUnit:getName() + local uGroup = theUnit:getGroup() + local gName = uGroup:getName() + for idx, theZone in pairs(williePete.wpZones) do - if theZone.checkedIn[uName] then return true end + if theZone.checkedIn[gName] then return true end end return false end @@ -426,6 +463,8 @@ function williePete.zedsDead(theObject) local theName = theObject:getName() -- now check if it's a registered blasted object:getSampleRate() + -- in multi-unit player groups, this can can lead to + -- mis-attribution, beware! local blaster = williePete.blastedObjects[theName] if blaster then local theUnit = Unit.getByName(blaster) @@ -459,7 +498,7 @@ function williePete:onEvent(event) local theUnit = event.initiator local pType = "(AI)" - if theUnit.getPlayerName then pType = "(" .. theUnit:getName() .. ")" end + if theUnit.getPlayerName and theUnit:getPlayerName() then pType = "(" .. theUnit:getName() .. ")" end if event.id == 1 then -- S_EVENT_SHOT -- initiator is who fired. maybe want to test if player @@ -470,7 +509,7 @@ function williePete:onEvent(event) end -- make sure that whoever fired it is being tracked by - -- a zone + -- a zone. zoneIsTracking checks on GROUP level! if not williePete.zoneIsTracking(theUnit) then return end @@ -479,6 +518,8 @@ function williePete:onEvent(event) local theWillie = {} theWillie.firedBy = theUnit:getName() theWillie.theUnit = theUnit + theWillie.theGroup = theUnit:getGroup() + theWillie.gName = theWillie.theGroup:getName() theWillie.weapon = event.weapon theWillie.wt = theWillie.weapon:getTypeName() theWillie.pos = theWillie.weapon:getPoint() @@ -486,15 +527,6 @@ 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 @@ -505,17 +537,15 @@ function williePete.isInside(theWillie) local theUnit = Unit.getByName(theUnitName) if not theUnit then return false end -- unit dead if not Unit.isExist(theUnit) then return false end -- dito - - local thePlayer = williePete.playerGUIs[theUnitName] - if not thePlayer then return nil end + local theGroup = theUnit:getGroup() + local gName = theGroup:getName() + local unitInfo = williePete.groupGUIs[gName] -- returns unitInfo struct, contains group info + if not unitInfo then return nil end for idx, theZone in pairs(williePete.wpZones) do if cfxZones.pointInZone(thePoint, theZone) then -- we are inside. but is this the right coalition? - if thePlayer.coa == theZone.coalition then - --trigger.action.outText("Willie in " .. theZone.name, 30) + if unitInfo.coa == theZone.coalition then return theZone - else - --trigger.action.outText("Willie wrong coa", 30) end -- if we want to allow neutral zones (doens't make sense) -- add another guard below @@ -532,8 +562,9 @@ function williePete.projectileHit(theWillie) local vmod = dcsCommon.vMultScalar(theWillie.v, 0.5 / williePete.ups) theWillie.pos = dcsCommon.vAdd(theWillie.pos, vmod) - -- reset last mark for player - local thePlayer = williePete.playerGUIs[theWillie.firedBy] + -- reset last mark for player's group + -- access unitInfo + local thePlayer = williePete.playerGUIs[theWillie.gName] thePlayer.pos = nil thePlayer.wpInZone = nil @@ -583,20 +614,25 @@ function williePete.playerUpdate() -- the zone that they checked in, or they are checked out --local zp = cfxZones.getPoint(theZone) for idy, unitInfo in pairs(theZone.checkedIn) do - -- make sure unit still exists + -- make sure at least one unit still exists local dropUnit = true - local theUnit = Unit.getByName(unitInfo.name) - if theUnit and Unit.isExist(theUnit) then - local up = theUnit:getPoint() - up.y = 0 - local isInside, dist = cfxZones.isPointInsideZone(up, theZone, theZone.checkInRange) - - if isInside then - dropUnit = false + local theGroup = Group.getByName(unitInfo.gName) + local allUnits = theGroup:getUnits() + for idx, theUnit in pairs(allUnits) do + --local theUnit = Unit.getByName(unitInfo.name) + if theUnit and Unit.isExist(theUnit) and + theUnit.getPlayerName and theUnit:getPlayerName() then + local up = theUnit:getPoint() + up.y = 0 + local isInside, dist = cfxZones.isPointInsideZone(up, theZone, theZone.checkInRange) + + if isInside then + dropUnit = false + end end end if dropUnit then - -- remove from zone check-in + -- all outside, remove from zone check-in -- williePete.doCheckOut(unitInfo) timer.scheduleFunction(williePete.doCheckOut, unitInfo, timer.getTime() + 0.1) -- to not muck up iteration end @@ -612,13 +648,10 @@ end function williePete.readConfigZone() local theZone = cfxZones.getZoneByName("wpConfig") if not theZone then - if williePete.verbose then - trigger.action.outText("+++wp: NO config zone!", 30) - end theZone = cfxZones.createSimpleZone("wpConfig") end - local facTypes = cfxZones.getStringFromZoneProperty(theZone, "facTypes", "all") + local facTypes = theZone:getStringFromZoneProperty("facTypes", "all") facTypes = string.upper(facTypes) -- make this an array @@ -631,19 +664,19 @@ function williePete.readConfigZone() williePete.facTypes = dcsCommon.trimArray(allTypes) -- how long a wp is active. must not be more than 5 minutes - williePete.wpMaxTime = cfxZones.getNumberFromZoneProperty(theZone, "wpMaxTime", 3 * 60) + williePete.wpMaxTime = theZone:getNumberFromZoneProperty( "wpMaxTime", 3 * 60) -- default check-in range, added to target zone's range and used -- for auto-check-out - williePete.checkInRange = cfxZones.getNumberFromZoneProperty(theZone, "checkInRange", 10000) -- 10 km outside + williePete.checkInRange = theZone:getNumberFromZoneProperty("checkInRange", 10000) -- 10 km outside - williePete.ackSound = cfxZones.getStringFromZoneProperty(theZone, "ackSound", "some") - williePete.guiSound = cfxZones.getStringFromZoneProperty(theZone, "guiSound", "some") + williePete.ackSound = theZone:getStringFromZoneProperty( "ackSound", "some") + williePete.guiSound = theZone:getStringFromZoneProperty( "guiSound", "some") - williePete.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + williePete.verbose = theZone.verbose if williePete.verbose then - trigger.action.outText("+++msgr: read config", 30) + trigger.action.outText("+++wp: read config", 30) end end diff --git a/modules/xFlags.lua b/modules/xFlags.lua index c582b59..8904840 100644 --- a/modules/xFlags.lua +++ b/modules/xFlags.lua @@ -1,5 +1,5 @@ xFlags = {} -xFlags.version = "1.3.1" +xFlags.version = "2.0.0" xFlags.verbose = false xFlags.hiVerbose = false xFlags.ups = 1 -- overwritten in get config when configZone is present @@ -11,30 +11,11 @@ xFlags.requiredLibs = { xFlags - flag array transmogrifier Version History - 1.0.0 - Initial version - 1.0.1 - allow flags names for ops as well - 1.1.0 - Watchflags harmonization - 1.2.0 - xDirect flag, - - direct array support - 1.2.1 - verbosity changes - - "most" operator - - "half or more" operator - - fixed reset - - xSuccess optimizations - - inc, dec, quoted flags - - matchNum can carry flag - 1.2.2 - on/off/suspend commands - - hiVerbose option - - corrected bug in reset checksum - 1.3.0 - xCount! flag - - "never" operator - 1.3.1 - guards for xtriggerOffFlag and xtriggerOnFlag - to prevent QoL warnings - - guards for xDirect - - guards for xCount - - - + 2.0.0 - dmlZones + - OOP + - xDirect# + - xCount# + - cleanup --]]-- xFlags.xFlagZones = {} @@ -62,11 +43,7 @@ end function xFlags.createXFlagsWithZone(theZone) local theArray = "" - if cfxZones.hasProperty(theZone, "xFlags") then - theArray = cfxZones.getStringFromZoneProperty(theZone, "xFlags", "") - else - theArray = cfxZones.getStringFromZoneProperty(theZone, "xFlags?", "") - end + theArray = theZone:getStringFromZoneProperty("xFlags?", "") -- now process the array and create the value arrays theZone.flagNames = cfxZones.flagArrayFromString(theArray) @@ -86,95 +63,72 @@ function xFlags.createXFlagsWithZone(theZone) end end theZone.xHasFired = false - if cfxZones.hasProperty(theZone, "xSuccess!") then - theZone.xSuccess = cfxZones.getStringFromZoneProperty(theZone, "xSuccess!", "") - end - - if cfxZones.hasProperty(theZone, "out!") then - theZone.xSuccess = cfxZones.getStringFromZoneProperty(theZone, "out!", "*") + if theZone:hasProperty("xSuccess!") then + theZone.xSuccess = theZone:getStringFromZoneProperty("xSuccess!", "") + elseif theZone:hasProperty("out!") then + theZone.xSuccess = theZone:getStringFromZoneProperty("out!", "*") end if not theZone.xSuccess then theZone.xSuccess = "*" end - if cfxZones.hasProperty(theZone, "xChange!") then - theZone.xChange = cfxZones.getStringFromZoneProperty(theZone, "xChange!", "*") + if theZone:hasProperty("xChange!") then + theZone.xChange = theZone:getStringFromZoneProperty("xChange!", "*") end - if cfxZones.hasProperty(theZone, "xDirect") then - theZone.xDirect = cfxZones.getStringFromZoneProperty(theZone, "xDirect", "*") + if theZone:hasProperty("xDirect#") then + theZone.xDirect = theZone:getStringFromZoneProperty("xDirect#", "*") end - if cfxZones.hasProperty(theZone, "xCount") then - theZone.xCount = cfxZones.getStringFromZoneProperty(theZone, "xCount", "*") + if theZone:hasProperty("xCount#") then + theZone.xCount = theZone:getStringFromZoneProperty("xCount#", "*") end - theZone.inspect = cfxZones.getStringFromZoneProperty(theZone, "require", "or") -- same as any + theZone.inspect = theZone:getStringFromZoneProperty("require", "or") -- same as any -- supported any/or, all/and, moreThan, atLeast, exactly theZone.inspect = string.lower(theZone.inspect) theZone.inspect = dcsCommon.trim(theZone.inspect) - theZone.matchNum = cfxZones.getStringFromZoneProperty(theZone, "#hits", "1") - - theZone.xTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "xFlagMethod", "change") -- (<>=[number or reference flag], off, on, yes, no, true, false, change - + theZone.matchNum = theZone:getStringFromZoneProperty("#hits", "1") -- string because can also be a flag ref + theZone.xTriggerMethod = theZone:getStringFromZoneProperty( "xFlagMethod", "change") + theZone.xTriggerMethod = string.lower(theZone.xTriggerMethod) theZone.xTriggerMethod = dcsCommon.trim(theZone.xTriggerMethod) - if cfxZones.hasProperty(theZone, "xReset?") then - theZone.xReset = cfxZones.getStringFromZoneProperty(theZone, "xReset?", "") - theZone.xLastReset = cfxZones.getFlagValue(theZone.xReset, theZone) + if theZone:hasProperty("xReset?") then + theZone.xReset = theZone:getStringFromZoneProperty("xReset?", "") + theZone.xLastReset = theZone:getFlagValue(theZone.xReset) end - theZone.xMethod = cfxZones.getStringFromZoneProperty(theZone, "xMethod", "inc") - if cfxZones.hasProperty(theZone, "method") then - theZone.xMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") + theZone.xMethod = theZone:getStringFromZoneProperty("xMethod", "inc") + if theZone:hasProperty("method") then + theZone.xMethod = theZone:getStringFromZoneProperty("method", "inc") end - theZone.xOneShot = cfxZones.getBoolFromZoneProperty(theZone, "oneShot", true) + theZone.xOneShot = theZone:getBoolFromZoneProperty("oneShot", true) -- on / off commands -- on/off flags - theZone.xSuspended = cfxZones.getBoolFromZoneProperty(theZone, "xSuspended", false) -- we are turned on + theZone.xSuspended = theZone:getBoolFromZoneProperty("xSuspended", false) -- we are turned on if theZone.xSuspended and (xFlags.verbose or theZone.verbose) then trigger.action.outText("+++xFlg: <" .. theZone.name .. "> starts suspended", 30) end - if cfxZones.hasProperty(theZone, "xOn?") then - theZone.xtriggerOnFlag = cfxZones.getStringFromZoneProperty(theZone, "xOn?", "*") - theZone.xlastTriggerOnValue = cfxZones.getFlagValue(theZone.xtriggerOnFlag, theZone) + if theZone:hasProperty("xOn?") then + theZone.xtriggerOnFlag = theZone:getStringFromZoneProperty("xOn?", "*") + theZone.xlastTriggerOnValue = theZone:getFlagValue(theZone.xtriggerOnFlag) end - if cfxZones.hasProperty(theZone, "xOff?") then - theZone.xtriggerOffFlag = cfxZones.getStringFromZoneProperty(theZone, "xOff?", "*") - theZone.xlastTriggerOffValue = cfxZones.getFlagValue(theZone.xtriggerOffFlag, theZone) + if theZone:hasProperty("xOff?") then + theZone.xtriggerOffFlag = theZone:getStringFromZoneProperty( "xOff?", "*") + theZone.xlastTriggerOffValue = theZone:getFlagValue(theZone.xtriggerOffFlag) end end function xFlags.evaluateNumOrFlag(theAttribute, theZone) - -- on entry, theAttribute contains a string - -- if it's a number, we return that, if it's a - -- string, we see if it's a quoted flag or - -- direct flag. in any way, we fetch and return - -- that flag's value - local aNum = tonumber(theAttribute) - if aNum then return aNum end - local remainder = dcsCommon.trim(theAttribute) - local esc = string.sub(remainder, 1, 1) - local last = string.sub(remainder, -1) - if esc == "(" and last == ")" and string.len(remainder) > 2 then - remainder = string.sub(remainder, 2, -2) - remainder = dcsCommon.trim(remainder) - end - - if esc == "\"" and last == "\"" and string.len(remainder) > 2 then - remainder = string.sub(remainder, 2, -2) - remainder = dcsCommon.trim(remainder) - end - - rNum = cfxZones.getFlagValue(remainder, theZone) + return cfxZones.evalRemainder(theAttribute, theZone) end function xFlags.evaluateFlags(theZone) @@ -184,7 +138,7 @@ function xFlags.evaluateFlags(theZone) -- since the checksum is order dependent, -- we must preserve the order of the array local flagName = theZone.flagNames[i] - currVals[i] = cfxZones.getFlagValue(flagName, theZone) + currVals[i] = theZone:getFlagValue(flagName) end -- now perform comparison flag by flag @@ -194,37 +148,10 @@ function xFlags.evaluateFlags(theZone) local firstChar = string.sub(op, 1, 1) local remainder = string.sub(op, 2) remainder = dcsCommon.trim(remainder) -- remove all leading and trailing spaces - local rNum = tonumber(remainder) - if not rNum then - -- interpret remainder as flag name - -- so we can say >*killMax or "22" with 22 a flag name - - -- we use remainder as name for flag - -- PROCESS ESCAPE SEQUENCES - local esc = string.sub(remainder, 1, 1) - local last = string.sub(remainder, -1) - if esc == "@" then - remainder = string.sub(remainder, 2) - remainder = dcsCommon.trim(remainder) - end - - if esc == "(" and last == ")" and string.len(remainder) > 2 then - -- note: iisues with startswith("(") ??? - remainder = string.sub(remainder, 2, -2) - remainder = dcsCommon.trim(remainder) - end - if esc == "\"" and last == "\"" and string.len(remainder) > 2 then - remainder = string.sub(remainder, 2, -2) - remainder = dcsCommon.trim(remainder) - end - if cfxZones.verbose then - trigger.action.outText("+++zne: accessing flag <" .. remainder .. ">", 30) - end - - rNum = cfxZones.getFlagValue(remainder, theZone) - end + + local rNum = theZone:evalRemainder(remainder) - -- this mimics cfxZones.testFlagByMethodForZone method (and is + -- the following mimics cfxZones.testFlagByMethodForZone method (and is -- that method's genesis), but is different enough not to invoke that -- method for i = 1, #theZone.flagNames do @@ -298,7 +225,6 @@ function xFlags.evaluateFlags(theZone) return 0, "" end if xFlags.verbose and lastHits ~= hits then - --trigger.action.outText("+++xF: hit detected for " .. theZone.flagNames[i] .. " in " .. theZone.name .. "(" .. op .. ")", 30) end end return hits, checkSum @@ -361,7 +287,7 @@ function xFlags.evaluateZone(theZone) end if theZone.xChange then - cfxZones.pollFlag(theZone.xChange, theZone.xMethod, theZone) + theZone:pollFlag(theZone.xChange, theZone.xMethod) if xFlags.verbose then trigger.action.outText("+++xFlag: change bang! on " .. theZone.xChange .. " for " .. theZone.name, 30) end @@ -378,15 +304,15 @@ function xFlags.evaluateZone(theZone) -- true (1)/false(0), no matter if changed or not if theZone.xDirect then if evalResult then - cfxZones.setFlagValueMult(theZone.xDirect, 1, theZone) + theZone:setFlagValue(theZone.xDirect, 1) else - cfxZones.setFlagValueMult(theZone.xDirect, 0, theZone) + theZone:setFlagValue(theZone.xDirect, 0) end end -- directly set the xCount flag if theZone.xCount then - cfxZones.setFlagValueMult(theZone.xCount, hits, theZone) + theZone:setFlagValueMult(theZone.xCount, hits) end -- now see if we bang the output according to method @@ -394,7 +320,7 @@ function xFlags.evaluateZone(theZone) if xFlags.verbose or theZone.verbose then trigger.action.outText("+++xFlag: success bang! on <" .. theZone.xSuccess .. "> for <" .. theZone.name .. "> with method <" .. theZone.xMethod .. ">", 30) end - cfxZones.pollFlag(theZone.xSuccess, theZone.xMethod, theZone) + theZone:pollFlag(theZone.xSuccess, theZone.xMethod) theZone.xHasFired = true end end @@ -407,14 +333,14 @@ function xFlags.update() for idx, theZone in pairs (xFlags.xFlagZones) do -- see if we should suspend - if theZone.xtriggerOnFlag and cfxZones.testZoneFlag(theZone, theZone.xtriggerOnFlag, "change", "xlastTriggerOnValue") then + if theZone.xtriggerOnFlag and theZone:testZoneFlag( 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 theZone.xtriggerOffFlag and cfxZones.testZoneFlag(theZone, theZone.xtriggerOffFlag, "change", "xlastTriggerOffValue") then + if theZone.xtriggerOffFlag and theZone:testZoneFlag( theZone.xtriggerOffFlag, "change", "xlastTriggerOffValue") then if xFlags.verbose or theZone.verbose then trigger.action.outText("+++xFlg: DISabling " .. theZone.name, 30) end @@ -428,7 +354,7 @@ function xFlags.update() -- see if they should reset if theZone.xReset then - local currVal = cfxZones.getFlagValue(theZone.xReset, theZone) + local currVal = theZone:getFlagValue(theZone.xReset) if currVal ~= theZone.xLastReset then theZone.xLastReset = currVal if xFlags.verbose or theZone.verbose then @@ -446,14 +372,11 @@ function xFlags.readConfigZone() -- note: must match exactly!!!! local theZone = cfxZones.getZoneByName("xFlagsConfig") if not theZone then - if xFlags.verbose then - trigger.action.outText("***xFlag: NO config zone!", 30) - end - return + theZone = cfxZones.createSimpleZone("xFlagsConfig") end - xFlags.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) - xFlags.ups = cfxZones.getNumberFromZoneProperty(theZone, "ups", 1) + xFlags.verbose = theZone.verbose + xFlags.ups = theZone:getNumberFromZoneProperty("ups", 1) if xFlags.verbose then trigger.action.outText("***xFlg: read config", 30) @@ -476,9 +399,6 @@ function xFlags.start() -- process RND Zones local attrZones = cfxZones.getZonesWithAttributeNamed("xFlags") - - -- now create an rnd gen for each one and add them - -- to our watchlist for k, aZone in pairs(attrZones) do xFlags.createXFlagsWithZone(aZone) -- process attribute and add to zone xFlags.addxFlags(aZone) -- remember it @@ -509,4 +429,5 @@ end --[[-- Additional features: - make #hits compatible to flags and numbers + - autoReset -- can be done by short-circuiting xsuccess! into xReset? --]]-- \ No newline at end of file diff --git a/tutorial & demo missions/demo - Being persistent.miz b/tutorial & demo missions/demo - Being persistent.miz index 7d0f2f0..3e6760d 100644 Binary files a/tutorial & demo missions/demo - Being persistent.miz and b/tutorial & demo missions/demo - Being persistent.miz differ diff --git a/tutorial & demo missions/demo - Players in the Zone.miz b/tutorial & demo missions/demo - Players in the Zone.miz index b076c93..aa87e26 100644 Binary files a/tutorial & demo missions/demo - Players in the Zone.miz and b/tutorial & demo missions/demo - Players in the Zone.miz differ diff --git a/tutorial & demo missions/demo - Willie Nillie.miz b/tutorial & demo missions/demo - Willie Nillie.miz index fc19d12..00be50c 100644 Binary files a/tutorial & demo missions/demo - Willie Nillie.miz and b/tutorial & demo missions/demo - Willie Nillie.miz differ diff --git a/tutorial & demo missions/demo - artillery with UI.miz b/tutorial & demo missions/demo - artillery with UI.miz index eba3f6b..ffe0d8f 100644 Binary files a/tutorial & demo missions/demo - artillery with UI.miz and b/tutorial & demo missions/demo - artillery with UI.miz differ diff --git a/tutorial & demo missions/demo - boom boom.miz b/tutorial & demo missions/demo - boom boom.miz index 8f8685a..76bcf8f 100644 Binary files a/tutorial & demo missions/demo - boom boom.miz and b/tutorial & demo missions/demo - boom boom.miz differ diff --git a/tutorial & demo missions/demo - debug events and more.miz b/tutorial & demo missions/demo - debug events and more.miz new file mode 100644 index 0000000..2cd3aa7 Binary files /dev/null and b/tutorial & demo missions/demo - debug events and more.miz differ diff --git a/tutorial & demo missions/demo - helo cargo.miz b/tutorial & demo missions/demo - helo cargo.miz index 902901f..243ea67 100644 Binary files a/tutorial & demo missions/demo - helo cargo.miz and b/tutorial & demo missions/demo - helo cargo.miz differ diff --git a/tutorial & demo missions/demo - moving spawners II.miz b/tutorial & demo missions/demo - moving spawners II.miz index ff175b4..5cc6219 100644 Binary files a/tutorial & demo missions/demo - moving spawners II.miz and b/tutorial & demo missions/demo - moving spawners II.miz differ diff --git a/tutorial & demo missions/demo - moving spawners.miz b/tutorial & demo missions/demo - moving spawners.miz index c6ed24e..d7f0f7c 100644 Binary files a/tutorial & demo missions/demo - moving spawners.miz and b/tutorial & demo missions/demo - moving spawners.miz differ diff --git a/tutorial & demo missions/demo - once, twice, three times a maybe.miz b/tutorial & demo missions/demo - once, twice, three times a maybe.miz index 4dd5c14..d51a327 100644 Binary files a/tutorial & demo missions/demo - once, twice, three times a maybe.miz and b/tutorial & demo missions/demo - once, twice, three times a maybe.miz differ diff --git a/tutorial & demo missions/demo - player score.miz b/tutorial & demo missions/demo - player score.miz index 4125ed5..6245583 100644 Binary files a/tutorial & demo missions/demo - player score.miz and b/tutorial & demo missions/demo - player score.miz differ diff --git a/tutorial & demo missions/demo - xFlags - Field Day.miz b/tutorial & demo missions/demo - xFlags - Field Day.miz index 113bed4..f4c286e 100644 Binary files a/tutorial & demo missions/demo - xFlags - Field Day.miz and b/tutorial & demo missions/demo - xFlags - Field Day.miz differ