diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index b6a5df3..4b5aeec 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 b8ea9b4..7ec5e87 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/RNDFlags.lua b/modules/RNDFlags.lua index 29b0a40..71a31fb 100644 --- a/modules/RNDFlags.lua +++ b/modules/RNDFlags.lua @@ -1,5 +1,5 @@ rndFlags = {} -rndFlags.version = "1.4.0" +rndFlags.version = "1.4.1" rndFlags.verbose = false rndFlags.requiredLibs = { "dcsCommon", -- always @@ -32,6 +32,7 @@ rndFlags.requiredLibs = { 1.3.2 - moved flagArrayFromString to dcsCommon - minor clean-up 1.4.0 - persistence + 1.4.1 - a little less verbosity --]] @@ -336,8 +337,10 @@ function rndFlags.start() -- process RND Zones local attrZones = cfxZones.getZonesWithAttributeNamed("RND!") - a = dcsCommon.getSizeOfTable(attrZones) - trigger.action.outText("RND! zones: " .. a, 30) + if rndFlags.verbose then + local a = dcsCommon.getSizeOfTable(attrZones) + trigger.action.outText("RND! zones: " .. a, 30) + end -- now create an rnd gen for each one and add them -- to our watchlist diff --git a/modules/cfxMX.lua b/modules/cfxMX.lua index 9daeb2f..c411f87 100644 --- a/modules/cfxMX.lua +++ b/modules/cfxMX.lua @@ -1,5 +1,5 @@ cfxMX = {} -cfxMX.version = "1.2.2" +cfxMX.version = "1.2.3" cfxMX.verbose = false --[[-- Mission data decoder. Access to ME-built mission structures @@ -21,10 +21,15 @@ cfxMX.verbose = false 1.2.2 - fixed ctry bug in countryByName - playerGroupByName - playerUnitByName + 1.2.3 - groupTypeByName + - groupCoalitionByName + --]]-- cfxMX.groupNamesByID = {} cfxMX.groupIDbyName = {} cfxMX.groupDataByName = {} +cfxMX.groupTypeByName = {} -- category of group: "helicopter", "plane", "ship"... +cfxMX.groupCoalitionByName = {} cfxMX.countryByName ={} cfxMX.linkByName = {} cfxMX.allFixedByName = {} @@ -205,11 +210,12 @@ function cfxMX.createCrossReferences() linkUnit = group_data.route.points[1].linkUnit cfxMX.linkByName[aName] = linkUnit end - + cfxMX.groupTypeByName[aName] = category cfxMX.groupNamesByID[aID] = aName cfxMX.groupIDbyName[aName] = aID cfxMX.groupDataByName[aName] = group_data cfxMX.countryByName[aName] = countryID -- !!! was cntry_id + cfxMX.groupCoalitionByName[aName] = coaNum -- now make the type-specific xrefs if obj_type_name == "helicopter" then diff --git a/modules/cfxReconMode.lua b/modules/cfxReconMode.lua index 12fc38b..0d6389e 100644 --- a/modules/cfxReconMode.lua +++ b/modules/cfxReconMode.lua @@ -1,5 +1,5 @@ cfxReconMode = {} -cfxReconMode.version = "2.1.1" +cfxReconMode.version = "2.1.2" cfxReconMode.verbose = false -- set to true for debug info cfxReconMode.reconSound = "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" -- to be played when somethiong discovered @@ -79,6 +79,9 @@ VERSION HISTORY - activate / deactivate by flags 2.1.1 - Lat Lon and MGRS also give Elevation - cfxReconMode.reportTime + 2.1.2 - imperialUnits for elevation + - wildcard in message format + - fix for mgrs bug in message (zone coords, not unit) cfxReconMode is a script that allows units to perform reconnaissance missions and, after detecting units, marks them on the map with @@ -424,13 +427,21 @@ function cfxReconMode.getLocation(theGroup) local theUnit = theGroup:getUnit(1) local currPoint = theUnit:getPoint() local ele = math.floor(land.getHeight({x = currPoint.x, y = currPoint.z})) + local units = "m" + if cfxReconMode.imperialUnits then + ele = math.floor(ele * 3.28084) -- feet + units = "ft" + else + ele = math.floor(ele) -- meters + end + if cfxReconMode.mgrs then local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint)) - msg = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing .. " Ele " .. ele .."m" + msg = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing .. " Ele " .. ele .. units else local lat, lon, alt = coord.LOtoLL(currPoint) lat, lon = dcsCommon.latLon2Text(lat, lon) - msg = "Lat " .. lat .. " Lon " .. lon .. " Ele " .. ele .."m" + msg = "Lat " .. lat .. " Lon " .. lon .. " Ele " .. ele ..units end return msg end @@ -483,12 +494,21 @@ function cfxReconMode.processZoneMessage(inMsg, theZone, theGroup) local theUnit = dcsCommon.getFirstLivingUnit(theGroup) currPoint = theUnit:getPoint() end + local ele = math.floor(land.getHeight({x = currPoint.x, y = currPoint.z})) + local units = "m" + if cfxReconMode.imperialUnits then + ele = math.floor(ele * 3.28084) -- feet + units = "ft" + else + ele = math.floor(ele) -- meters + end local lat, lon, alt = coord.LOtoLL(currPoint) lat, lon = dcsCommon.latLon2Text(lat, lon) outMsg = outMsg:gsub("", lat) outMsg = outMsg:gsub("", lon) - currPoint = cfxZones.getPoint(theZone) + outMsg = outMsg:gsub("", ele..units) + --currPoint = cfxZones.getPoint(theZone) local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint)) local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing outMsg = outMsg:gsub("", mgrs) @@ -1019,6 +1039,10 @@ function cfxReconMode.readConfigZone() cfxReconMode.lastDeActivate = cfxZones.getFlagValue(cfxReconMode.deactivate, theZone) end + cfxReconMode.imperialUnits = cfxZones.getBoolFromZoneProperty(theZone, "imperial", false) + if cfxZones.hasProperty(theZone, "imperialUnits") then + cfxReconMode.imperialUnits = cfxZones.getBoolFromZoneProperty(theZone, "imperialUnits", false) + end cfxReconMode.theZone = theZone -- save this zone end diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index ffe218a..4b61c2d 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -1,5 +1,5 @@ cloneZones = {} -cloneZones.version = "1.5.2" +cloneZones.version = "1.5.4" cloneZones.verbose = false cloneZones.requiredLibs = { "dcsCommon", -- always @@ -59,6 +59,8 @@ cloneZones.allCObjects = {} -- all clones objects 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 @@ -148,7 +150,7 @@ function cloneZones.allGroupsInZoneByData(theZone) end function cloneZones.createClonerWithZone(theZone) -- has "Cloner" - if cloneZones.verbose then + if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: new cloner " .. theZone.name, 30) end @@ -292,6 +294,11 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" if cfxZones.hasProperty(theZone, "rndLoc") then theZone.rndLoc = cfxZones.getBoolFromZoneProperty(theZone, "rndLoc", false) end + theZone.centerOnly = cfxZones.getBoolFromZoneProperty(theZone, "centerOnly", false) + if cfxZones.hasProperty(theZone, "wholeGroups") then + theZone.centerOnly = cfxZones.getBoolFromZoneProperty(theZone, "wholeGroups", false) + end + theZone.rndHeading = cfxZones.getBoolFromZoneProperty(theZone, "rndHeading", false) theZone.onRoad = cfxZones.getBoolFromZoneProperty(theZone, "onRoad", false) @@ -308,13 +315,16 @@ end -- function cloneZones.despawnAll(theZone) - if cloneZones.verbose then - trigger.action.outText("wiping <" .. theZone.name .. ">", 30) + 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) if aGroup:isExist() then + if theZone.verbose then + trigger.action.outText("+++clnZ: will destroy <" .. aGroup:getName() .. ">", 30) + end cloneZones.invokeCallbacks(theZone, "will despawn group", aGroup) Group.destroy(aGroup) end @@ -323,7 +333,7 @@ function cloneZones.despawnAll(theZone) -- warning! may be mismatch because we are looking at groups -- not objects. let's see if aStatic:isExist() then - if cloneZones.verbose then + if cloneZones.verbose or theZone.verbose then trigger.action.outText("Destroying static <" .. aStatic:getName() .. ">", 30) end cloneZones.invokeCallbacks(theZone, "will despawn static", aStatic) @@ -334,12 +344,47 @@ function cloneZones.despawnAll(theZone) theZone.myStatics = {} end -function cloneZones.updateLocationsInGroupData(theData, zoneDelta, adjustAllWaypoints) +function cloneZones.assignClosestParking(theData) + -- on enter: theData has units with updated x, y + -- and waypoint 1 action is From Parking + -- and it has at least one unit + -- let's get the airbase + local theRoute = theData.route -- we know it exists + local thePoints = theRoute.points + local firstPoint = thePoints[1] + local loc = {} + loc.x = firstPoint.x + loc.y = 0 + loc.z = firstPoint.y + local theAirbase = dcsCommon.getClosestAirbaseTo(loc) + -- now let's assign free slots closest to unit + local slotsTaken = {} + local units = theData.units + local cat = cfxMX.groupTypeByName[theData.name] + for idx, theUnit in pairs(units) do + local newSlot = dcsCommon.getClosestFreeSlotForCatInAirbaseTo(cat, theUnit.x, theUnit.y, theAirbase, slotsTaken) + if newSlot then + local slotNo = newSlot.Term_Index + --trigger.action.outText("unit <" .. theUnit.name .. "> old slot <" .. theUnit.parking .. "> to new slot <" .. slotNo .. ">", 30) + theUnit.parking_id = nil -- !! or you b screwed + theUnit.parking = slotNo -- !! screw parking_ID, they don't match + theUnit.x = newSlot.vTerminalPos.x + theUnit.y = newSlot.vTerminalPos.z -- !!! + table.insert(slotsTaken, slotNo) + end + end +end + +function cloneZones.updateLocationsInGroupData(theData, zoneDelta, adjustAllWaypoints) + -- enter with theData being group's data block -- remember that zoneDelta's [z] modifies theData's y!! theData.x = theData.x + zoneDelta.x theData.y = theData.y + zoneDelta.z -- !!! local units = theData.units + local departFromAerodrome = false + --local departingAerodrome + local fromParking = false for idx, aUnit in pairs(units) do aUnit.x = aUnit.x + zoneDelta.x aUnit.y = aUnit.y + zoneDelta.z -- again!!!! @@ -373,8 +418,11 @@ function cloneZones.updateLocationsInGroupData(theData, zoneDelta, adjustAllWayp loc.y = 0 loc.z = firstPoint.y local bestAirbase = dcsCommon.getClosestAirbaseTo(loc) + --departingAerodrome = bestAirbase firstPoint.airdromeId = bestAirbase:getID() -- trigger.action.outText("first: adjusted to " .. firstPoint.airdromeId, 30) + departFromAerodrome = true + fromParking = dcsCommon.stringStartsWith(firstPoint.action, "From Parking") end -- adjust last point (landing) @@ -393,8 +441,20 @@ function cloneZones.updateLocationsInGroupData(theData, zoneDelta, adjustAllWayp end end + end -- if theRoute + + -- now process departing slot if given + if departFromAerodrome then + -- we may need alt from land to add here, maybe later + + -- now process parking slots, and choose closest slot + -- per unit's location + if fromParking then + cloneZones.assignClosestParking(theData) + end end end + function cloneZones.uniqueID() local uid = cloneZones.uniqueCounter cloneZones.uniqueCounter = cloneZones.uniqueCounter + 1 @@ -670,6 +730,9 @@ function cloneZones.handoffTracking(theGroup, theZone) end function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) + if cloneZones.verbose or spawnZone.verbose then + trigger.action.outText("+++clnZ: spawning with template <" .. theZone.name .. "> for spawner <" .. spawnZone.name .. ">", 30) + end -- theZone is the cloner with the template -- spawnZone is the spawner with settings -- if not spawnZone then spawnZone = theZone end @@ -697,17 +760,34 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) local theCat = cfxMX.catText2ID(cat) rawData.CZtheCat = theCat -- save cat - -- update their position if not spawning to exact same location + -- update their position if not spawning to exact same location + if cloneZones.verbose or theZone.verbose then + trigger.action.outText("+++clnZ: tmpl delta x = <" .. math.floor(zoneDelta.x) .. ">, y = <" .. math.floor(zoneDelta.z) .. "> for tmpl <" .. theZone.name .. "> to cloner <" .. spawnZone.name .. ">", 30) + end cloneZones.updateLocationsInGroupData(rawData, zoneDelta, spawnZone.moveRoute) -- apply randomizer if selected + if spawnZone.rndLoc then + --trigger.action.outText("rndloc for <" .. spawnZone.name .. ">", 30) + -- calculate the entire group's displacement local units = rawData.units + local r = math.random() * spawnZone.radius + local phi = 6.2831 * math.random() -- that's 2Pi, folx + local dx = r * math.cos(phi) + local dy = r * math.sin(phi) + for idx, aUnit in pairs(units) do - local r = math.random() * spawnZone.radius - local phi = 6.2831 * math.random() -- that's 2Pi, folx - local dx = r * math.cos(phi) - local dy = r * math.sin(phi) + if not spawnZone.centerOnly then + -- *every unit's displacement is randomized + r = math.random() * spawnZone.radius + phi = 6.2831 * math.random() -- that's 2Pi, folx + dx = r * math.cos(phi) + dy = r * math.sin(phi) + end + if spawnZone.verbose or cloneZones.verbose then + trigger.action.outText("+++clnZ: <" .. spawnZone.name .. "> R = " .. spawnZone.radius .. ":G<" .. rawData.name .. "/" .. aUnit.name .. "> - rndLoc: r = " .. r .. ", dx = " .. dx .. ", dy= " .. dy .. ".", 30) + end aUnit.x = aUnit.x + dx aUnit.y = aUnit.y + dy end @@ -715,44 +795,71 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) if spawnZone.rndHeading then local units = rawData.units - for idx, aUnit in pairs(units) do - local phi = 6.2831 * math.random() -- that's 2Pi, folx - aUnit.heading = phi + if spawnZone.centerOnly and units and units[1] then + -- rotate entire group around unit 1 + local cx = units[1].x + local cy = units[1].y + local degrees = 360 * math.random() -- rotateGroupData uses degrees + dcsCommon.rotateGroupData(rawData, degrees, cx, cy) + else + for idx, aUnit in pairs(units) do + local phi = 6.2831 * math.random() -- that's 2Pi, folx + aUnit.heading = phi + end end end -- apply onRoad option if selected if spawnZone.onRoad then local units = rawData.units - local iterCount = 0 - local otherLocs = {} -- resolved locs - for idx, aUnit in pairs(units) do - local cx = aUnit.x - local cy = aUnit.y - -- we now iterate until there is enough separation or too many iters - local tooClose - local np, nx, ny - repeat - nx, ny = land.getClosestPointOnRoads("roads", cx, cy) - -- compare this with all other locs - np = {x=nx, y=ny} - tooClose = false - for idc, op in pairs(otherLocs) do - local d = dcsCommon.dist(np, op) - if d < cloneZones.minSep then - tooClose = true - cx = cx + cloneZones.minSep - cy = cy + cloneZones.minSep - iterCount = iterCount + 1 - -- trigger.action.outText("d fail for <" .. aUnit.name.. ">: d= <" .. d .. ">, iters = <" .. iterCount .. ">", 30) - end - end - until (iterCount > cloneZones.maxIter) or (not tooClose) - -- trigger.action.outText("separation iters for <" .. aUnit.name.. ">:<" .. iterCount .. ">", 30) - table.insert(otherLocs, np) - aUnit.x = nx - aUnit.y = ny - end + if spawnZone.centerOnly then + -- only place the first unit in group on roads + -- and displace all other with the same offset + local hasOffset = false + local dx, dy, cx, cy + for idx, aUnit in pairs(units) do + cx = aUnit.x + cy = aUnit.y + if not hasOffset then + local nx, ny = land.getClosestPointOnRoads("roads", cx, cy) + dx = nx - cx + dy = ny - cy + hasOffset = true + end + aUnit.x = cx + dx + aUnit.y = cy + dy + end + else + local iterCount = 0 + local otherLocs = {} -- resolved locs + for idx, aUnit in pairs(units) do + local cx = aUnit.x + local cy = aUnit.y + -- we now iterate until there is enough separation or too many iters + local tooClose + local np, nx, ny + repeat + nx, ny = land.getClosestPointOnRoads("roads", cx, cy) + -- compare this with all other locs + np = {x=nx, y=ny} + tooClose = false + for idc, op in pairs(otherLocs) do + local d = dcsCommon.dist(np, op) + if d < cloneZones.minSep then + tooClose = true + cx = cx + cloneZones.minSep + cy = cy + cloneZones.minSep + iterCount = iterCount + 1 + -- trigger.action.outText("d fail for <" .. aUnit.name.. ">: d= <" .. d .. ">, iters = <" .. iterCount .. ">", 30) + end + end + until (iterCount > cloneZones.maxIter) or (not tooClose) + -- trigger.action.outText("separation iters for <" .. aUnit.name.. ">:<" .. iterCount .. ">", 30) + table.insert(otherLocs, np) + aUnit.x = nx + aUnit.y = ny + end + end -- else centerOnly end @@ -946,6 +1053,7 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) end function cloneZones.spawnWithCloner(theZone) + trigger.action.outText("+++clnZ: enter spawnWithCloner for <" .. theZone.name .. ">", 30) if not theZone then trigger.action.outText("+++clnZ: nil zone on spawnWithCloner", 30) return @@ -966,14 +1074,17 @@ function cloneZones.spawnWithCloner(theZone) local templates = dcsCommon.splitString(templateName, ",") templateName = dcsCommon.pickRandom(templates) templateName = dcsCommon.trim(templateName) - if cloneZones.verbose then + if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: picked random template <" .. templateName .."> for from <" .. allNames .. "> for cloner " .. theZone.name, 30) end end + if cloneZones.verbose or theZone.verbose then + trigger.action.outText("+++clnZ: spawning - picked <" .. templateName .. "> as template", 30) + end local newTemplate = cloneZones.getCloneZoneByName(templateName) if not newTemplate then - if cloneZones.verbose then + if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: no clone source with name <" .. templateName .."> for cloner " .. theZone.name, 30) end return @@ -1118,10 +1229,10 @@ function cloneZones.update() end end -function cloneZones.onStart() - --trigger.action.outText("+++clnZ: Enter atStart", 30) +function cloneZones.doOnStart() for idx, theZone in pairs(cloneZones.cloners) do if theZone.onStart then + trigger.action.outText("+++clnZ: onStart true for <" .. theZone.name .. ">", 30) if theZone.isStarted then if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnz: onStart pre-empted for <" .. theZone.name .. "> by persistence", 30) @@ -1422,7 +1533,7 @@ function cloneZones.start() -- cycles to go through object removal -- persistencey has loaded isStarted if a cloner was -- already started - timer.scheduleFunction(cloneZones.onStart, {}, timer.getTime() + 0.1) + timer.scheduleFunction(cloneZones.doOnStart, {}, timer.getTime() + 1.0) -- start update cloneZones.update() diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index f15a249..878a7ca 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "2.7.1" +dcsCommon.version = "2.7.2" --[[-- VERSION HISTORY 2.2.6 - compassPositionOfARelativeToB - clockPositionOfARelativeToB @@ -52,7 +52,7 @@ dcsCommon.version = "2.7.1" - dcsCommon.trimArray( - createStaticObjectData uses trim for type - getEnemyCoalitionFor understands strings, still returns number - - coalition2county also undertsands 'red' and 'blue' + - 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() @@ -92,6 +92,12 @@ dcsCommon.version = "2.7.1" 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() --]]-- @@ -388,6 +394,64 @@ dcsCommon.version = "2.7.1" return closestBase, delta end + function dcsCommon.getClosestFreeSlotForCatInAirbaseTo(cat, x, y, theAirbase, ignore) + if not theAirbase then return nil end + if not ignore then ignore = {} end + if not cat then return nil end + if (not cat == "helicopter") and (not cat == "plane") then + trigger.action.outText("+++common-getslotforcat: wrong cat <" .. cat .. ">", 30) + return nil + end + local allFree = theAirbase:getParking(true) -- only free slots + local filterFreeByType = {} + for idx, aSlot in pairs(allFree) do + local termT = aSlot.Term_Type + if termT == 104 or + (termT == 72 and cat == "plane") or + (termT == 68 and cat == "plane") or + (termT == 40 and cat == "helicopter") then + table.insert(filterFreeByType, aSlot) + else + -- we skip this slot, not good for type + end + end + + if #filterFreeByType == 0 then + return nil + end + + local reallyFree = {} + for idx, aSlot in pairs(filterFreeByType) do + local slotNum = aSlot.Term_Index + isTaken = false + for idy, taken in pairs(ignore) do + if taken == slotNum then isTaken = true end + end + if not isTaken then + table.insert(reallyFree, aSlot) + end + end + + if #reallyFree < 1 then + reallyFree = filterFreeByType + end + + local closestDist = math.huge + local closestSlot = nil + local p = {x = x, y = 0, z = y} -- !! + 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 + -- -- U N I T S M A N A G E M E N T -- @@ -1823,6 +1887,7 @@ end end function dcsCommon.stringStartsWith(theString, thePrefix) + if not theString then return false end return theString:find(thePrefix) == 1 end @@ -2473,6 +2538,7 @@ function dcsCommon.flagArrayFromString(inString, verbose) local rawElements = dcsCommon.splitString(inString, ",") -- go over all elements for idx, anElement in pairs(rawElements) do + anElement = dcsCommon.trim(anElement) if dcsCommon.stringStartsWithDigit(anElement) and dcsCommon.containsString(anElement, "-") then -- interpret this as a range local theRange = dcsCommon.splitString(anElement, "-") @@ -2498,7 +2564,7 @@ function dcsCommon.flagArrayFromString(inString, verbose) end else -- single number - f = dcsCommon.trim(anElement) -- DML flag upgrade: accept strings tonumber(anElement) + local f = dcsCommon.trim(anElement) -- DML flag upgrade: accept strings tonumber(anElement) if f then table.insert(flags, f) @@ -2513,6 +2579,81 @@ function dcsCommon.flagArrayFromString(inString, verbose) return flags end +function dcsCommon.rangeArrayFromString(inString, verbose) + if not verbose then verbose = false end + + if verbose then + trigger.action.outText("+++rangeArray: processing <" .. inString .. ">", 30) + end + + if string.len(inString) < 1 then + trigger.action.outText("+++rangeArray: empty ranges", 30) + return {} + end + + local ranges = {} + local rawElements = dcsCommon.splitString(inString, ",") + -- go over all elements + for idx, anElement in pairs(rawElements) do + anElement = dcsCommon.trim(anElement) + local outRange = {} + if dcsCommon.stringStartsWithDigit(anElement) and dcsCommon.containsString(anElement, "-") then + -- interpret this as a range + local theRange = dcsCommon.splitString(anElement, "-") + local lowerBound = theRange[1] + lowerBound = tonumber(lowerBound) + local upperBound = theRange[2] + upperBound = tonumber(upperBound) + if lowerBound and upperBound then + -- swap if wrong order + if lowerBound > upperBound then + local temp = upperBound + upperBound = lowerBound + lowerBound = temp + end + -- now add to ranges + outRange[1] = lowerBound + outRange[2] = upperBound + table.insert(ranges, outRange) + if verbose then + trigger.action.outText("+++rangeArray: new range <" .. lowerBound .. "> to <" .. upperBound .. ">", 30) + end + else + -- bounds illegal + trigger.action.outText("+++rangeArray: ignored range <" .. anElement .. "> (range)", 30) + end + else + -- single number + local f = dcsCommon.trim(anElement) + f = tonumber(f) + if f then + outRange[1] = f + outRange[2] = f + table.insert(ranges, outRange) + if verbose then + trigger.action.outText("+++rangeArray: new (single-val) range <" .. f .. "> to <" .. f .. ">", 30) + end + else + trigger.action.outText("+++rangeArray: ignored element <" .. anElement .. "> (single)", 30) + end + end + end + if verbose then + trigger.action.outText("+++rangeArray: <" .. #ranges .. "> ranges total", 30) + end + return ranges +end + +function dcsCommon.incFlag(flagName) + local v = trigger.misc.getUserFlag(flagName) + trigger.action.setUserFlag(flagName, v + 1) +end + +function dcsCommon.decFlag(flagName) + local v = trigger.misc.getUserFlag(flagName) + trigger.action.setUserFlag(flagName, v - 1) +end + function dcsCommon.objectHandler(theObject, theCollector) table.insert(theCollector, theObject) return true diff --git a/modules/groupTrackers.lua b/modules/groupTrackers.lua index d2874b5..2db6007 100644 --- a/modules/groupTrackers.lua +++ b/modules/groupTrackers.lua @@ -1,5 +1,5 @@ groupTracker = {} -groupTracker.version = "1.2.0" +groupTracker.version = "1.2.1" groupTracker.verbose = false groupTracker.ups = 1 groupTracker.requiredLibs = { @@ -24,10 +24,11 @@ groupTracker.trackers = {} - allGone! output - triggerMethod - method - - isDead optimization + - isDead optimiz ation 1.2.0 - double detection - numUnits output - persistence + 1.2.1 - allGone! bug removed --]]-- @@ -362,7 +363,7 @@ function groupTracker.update() -- see if we need to bang on empty! local currCount = #theZone.trackedGroups if theZone.allGoneFlag and currCount == 0 and currCount ~= theZone.lastGroupCount then - cfxZones.pollFlag(aZone.allGoneFlag, aZone.trackerMethod, aZone) + cfxZones.pollFlag(theZone.allGoneFlag, theZone.trackerMethod, theZone) end theZone.lastGroupCount = currCount end diff --git a/modules/messenger.lua b/modules/messenger.lua index c910821..626d573 100644 --- a/modules/messenger.lua +++ b/modules/messenger.lua @@ -1,5 +1,5 @@ messenger = {} -messenger.version = "1.3.3" +messenger.version = "2.0.0" messenger.verbose = false messenger.requiredLibs = { "dcsCommon", -- always @@ -26,6 +26,22 @@ messenger.messengers = {} - 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 --]]-- @@ -48,6 +64,7 @@ end -- read attributes -- function messenger.preProcMessage(inMsg, theZone) + -- Replace STATIC bits of message like CR and zone name if not inMsg then return "" end local formerType = type(inMsg) if formerType ~= "string" then inMsg = tostring(inMsg) end @@ -58,27 +75,172 @@ function messenger.preProcMessage(inMsg, theZone) if theZone then outMsg = outMsg:gsub("", theZone.name) end + return outMsg +end + +-- Old-school processing to replace wildcards +-- repalce with current time +-- replace with zone's current lonlat +-- replace with zone's current mgrs +function messenger.dynamicProcessClassic(inMsg, theZone) + + if not inMsg then return "" end -- replace with current mission time HMS local absSecs = timer.getAbsTime()-- + env.mission.start_time while absSecs > 86400 do absSecs = absSecs - 86400 -- subtract out all days end - local timeString = dcsCommon.processHMS("<:h>:<:m>:<:s>", absSecs) - outMsg = outMsg:gsub("", timeString) + local timeString = dcsCommon.processHMS(theZone.msgTimeFormat, absSecs) + local outMsg = inMsg:gsub("", timeString) -- replace with lat of zone point and with lon of zone point -- and with mgrs coords of zone point local currPoint = cfxZones.getPoint(theZone) - local lat, lon, alt = coord.LOtoLL(currPoint) + local lat, lon = coord.LOtoLL(currPoint) lat, lon = dcsCommon.latLon2Text(lat, lon) + local alt = land.getHeight({x = currPoint.x, y = currPoint.z}) + if theZone.imperialUnits then + alt = math.floor(alt * 3.28084) -- feet + else + alt = math.floor(alt) -- meters + end outMsg = outMsg:gsub("", lat) outMsg = outMsg:gsub("", lon) + outMsg = outMsg:gsub("", alt) local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint)) local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing outMsg = outMsg:gsub("", mgrs) return outMsg end +-- +-- new dynamic flag processing +-- +function messenger.processDynamicValues(inMsg, theZone) + -- replace all occurences of with their values + local pattern = "" -- no list allowed but blanks and * and . and - and _ --> we fail on the other specials to keep this simple + local outMsg = inMsg + repeat -- iterate all patterns one by one + local startLoc, endLoc = string.find(outMsg, pattern) + if startLoc then + local theValParam = string.sub(outMsg, startLoc, endLoc) + -- strip lead and trailer + local param = string.gsub(theValParam, "","") + -- param = dcsCommon.trim(param) -- trim is called anyway + -- access flag + local val = cfxZones.getFlagValue(param, theZone) + val = tostring(val) + if not val then val = "NULL" end + -- replace pattern in original with new val + outMsg = string.gsub(outMsg, pattern, val, 1) -- only one sub! + end + until not startLoc + return outMsg +end + +function messenger.processDynamicTime(inMsg, theZone) + -- replace all occurences of with their values + local pattern = "" -- no list allowed but blanks and * and . and - and _ --> we fail on the other specials to keep this simple + local outMsg = inMsg + repeat -- iterate all patterns one by one + local startLoc, endLoc = string.find(outMsg, pattern) + if startLoc then + local theValParam = string.sub(outMsg, startLoc, endLoc) + -- strip lead and trailer + local param = string.gsub(theValParam, "","") + -- access flag + local val = cfxZones.getFlagValue(param, theZone) + -- use this to process as time value + --trigger.action.outText("time: accessing <" .. param .. "> and received <" .. val .. ">", 30) + local timeString = dcsCommon.processHMS(theZone.msgTimeFormat, val) + + if not timeString then timeString = "NULL" end + -- replace pattern in original with new val + outMsg = string.gsub(outMsg, pattern, timeString, 1) -- only one sub! + end + until not startLoc + return outMsg +end + +function messenger.processDynamicLoc(inMsg, theZone) + -- replace all occurences of with their values + local locales = {"lat", "lon", "ele", "mgrs", "lle", "latlon"} + local outMsg = inMsg + for idx, aLocale in pairs(locales) do + local pattern = "<" .. aLocale .. ":%s*[%s%w%*%d%.%-_]+>" + repeat -- iterate all patterns one by one + local startLoc, endLoc = string.find(outMsg, pattern) + if startLoc then + local theValParam = string.sub(outMsg, startLoc, endLoc) + -- strip lead and trailer + local param = string.gsub(theValParam, "<" .. aLocale .. ":%s*", "") + param = string.gsub(param, ">","") + -- find zone or unit + param = dcsCommon.trim(param) + local thePoint = nil + local tZone = cfxZones.getZoneByName(param) + local tUnit = Unit.getByName(param) + if tZone then + thePoint = cfxZones.getPoint(tZone) + -- since zones always have elevation of 0, + -- now get the elevation from the map + thePoint.y = land.getHeight({x = thePoint.x, y = thePoint.z}) + elseif tUnit then + if Unit.isExist(tUnit) then + thePoint = tUnit:getPoint() + end + else + -- nothing to do, remove me. + end + + local locString = theZone.errString + if thePoint then + -- now that we have a point, we can do locale-specific + -- processing. return result in locString + local lat, lon, alt = coord.LOtoLL(thePoint) + lat, lon = dcsCommon.latLon2Text(lat, lon) + if theZone.imperialUnits then + alt = math.floor(alt * 3.28084) -- feet + else + alt = math.floor(alt) -- meters + end + if aLocale == "lat" then locString = lat + elseif aLocale == "lon" then locString = lon + elseif aLocale == "ele" then locString = tostring(alt) + elseif aLocale == "lle" then locString = lat .. " " .. lon .. " ele " .. tostring(alt) + elseif aLocale == "latlon" then locString = lat .. " " .. lon + else + -- we have mgrs + local grid = coord.LLtoMGRS(coord.LOtoLL(thePoint)) + locString = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing + end + end + -- replace pattern in original with new val + outMsg = string.gsub(outMsg, pattern, locString, 1) -- only one sub! + end -- if startloc + until not startLoc + end -- for all locales + return outMsg +end + +function messenger.dynamicFlagProcessing(inMsg, theZone) + if not inMsg then return "No in message" end + if not theZone then return "Nil zone" end + + -- process + local msg = messenger.processDynamicValues(inMsg, theZone) + + -- process + msg = messenger.processDynamicTime(msg, theZone) + + -- process lat / lon / ele / mgrs + msg = messenger.processDynamicLoc(msg, theZone) + + return msg +end + function messenger.createMessengerWithZone(theZone) -- start val - a range @@ -147,6 +309,7 @@ function messenger.createMessengerWithZone(theZone) theZone.lastMessageOn = cfxZones.getFlagValue(theZone.messageOnFlag, theZone) end + -- reveiver: coalition, group, unit if cfxZones.hasProperty(theZone, "coalition") then theZone.msgCoalition = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0) end @@ -155,11 +318,45 @@ function messenger.createMessengerWithZone(theZone) theZone.msgCoalition = cfxZones.getCoalitionFromZoneProperty(theZone, "msgCoalition", 0) end - -- flag whose value can be read + if cfxZones.hasProperty(theZone, "group") then + theZone.msgGroup = cfxZones.getStringFromZoneProperty(theZone, "group", "") + end + if cfxZones.hasProperty(theZone, "msgGroup") then + theZone.msgGroup = cfxZones.getStringFromZoneProperty(theZone, "msgGroup", "") + end + + if cfxZones.hasProperty(theZone, "unit") then + theZone.msgUnit = cfxZones.getStringFromZoneProperty(theZone, "unit", "") + end + if cfxZones.hasProperty(theZone, "msgUnit") then + theZone.msgUnit = cfxZones.getStringFromZoneProperty(theZone, "msgUnit", "") + end + + if (theZone.msgGroup and theZone.msgUnit) or + (theZone.msgGroup and theZone.msgCoalition) or + (theZone.msgUnit and theZone.msgCoalition) + then + trigger.action.outText("+++msg: WARNING - messenger in <" .. theZone.name .. "> has conflicting coalition, group and unit, use only one.", 30) + end + + -- flag whose value can be read: to be deprecated if cfxZones.hasProperty(theZone, "messageValue?") then theZone.messageValue = cfxZones.getStringFromZoneProperty(theZone, "messageValue?", "") end + -- time format for new + theZone.msgTimeFormat = cfxZones.getStringFromZoneProperty(theZone, "timeFormat", "<:h>:<:m>:<:s>") + + theZone.imperialUnits = cfxZones.getBoolFromZoneProperty(theZone, "imperial", false) + if cfxZones.hasProperty(theZone, "imperialUnits") then + theZone.imperialUnits = cfxZones.getBoolFromZoneProperty(theZone, "imperialUnits", false) + end + + theZone.errString = cfxZones.getStringFromZoneProperty(theZone, "error", "") + if cfxZones.hasProperty(theZone, "messageError") then + theZone.errString = cfxZones.getStringFromZoneProperty(theZone, "messageError", "") + end + if messenger.verbose or theZone.verbose then trigger.action.outText("+++Msg: new zone <".. theZone.name .."> will say <".. theZone.message .. ">", 30) end @@ -182,12 +379,22 @@ function messenger.getMessage(theZone) -- replace *zone and *value wildcards - msg = string.gsub(msg, "*name", zName) - msg = string.gsub(msg, "*value", zVal) + --msg = string.gsub(msg, "*name", zName)-- deprecated + --msg = string.gsub(msg, "*value", zVal) -- deprecated + -- old-school to provide value from messageValue msg = string.gsub(msg, "", zVal) local z = tonumber(zVal) if not z then z = 0 end msg = dcsCommon.processHMS(msg, z) + + -- process [classic format], and + msg = messenger.dynamicProcessClassic(msg, theZone) + + -- now add new processing of access + msg = messenger.dynamicFlagProcessing(msg, theZone) + + -- now add new processinf of + -- also handles , , return msg end @@ -212,6 +419,20 @@ function messenger.isTriggered(theZone) if theZone.msgCoalition then trigger.action.outTextForCoalition(theZone.msgCoalition, msg, theZone.duration, theZone.clearScreen) trigger.action.outSoundForCoalition(theZone.msgCoalition, fileName) + elseif theZone.msgGroup then + local theGroup = Group.getByName(theZone.msgGroup) + if theGroup and Group.isExist(theGroup) then + local ID = theGroup:getID() + trigger.action.outTextForGroup(ID, msg, theZone.duration, theZone.clearScreen) + trigger.action.outSoundForGroup(ID, fileName) + end + elseif theZone.msgUnit then + local theUnit = Unit.getByName(theZone.msgUnit) + if theUnit and Unit.isExist(theUnit) then + local ID = theUnit:getID() + trigger.action.outTextForUnit(ID, msg, theZone.duration, theZone.clearScreen) + trigger.action.outSoundForUnit(ID, fileName) + end else -- out to all trigger.action.outText(msg, theZone.duration, theZone.clearScreen) @@ -312,5 +533,10 @@ end --[[-- Wildcard extension: - messageValue supports multiple flags like 1-3, *hi ther, *bingo and then *value[name] returns that value + - general flag access + - + - + - + + --]]-- \ No newline at end of file diff --git a/modules/radioMenus.lua b/modules/radioMenus.lua index 9c3094e..cee7bca 100644 --- a/modules/radioMenus.lua +++ b/modules/radioMenus.lua @@ -1,5 +1,5 @@ radioMenu = {} -radioMenu.version = "1.1.0" +radioMenu.version = "2.0.0" radioMenu.verbose = false radioMenu.ups = 1 radioMenu.requiredLibs = { @@ -15,8 +15,18 @@ radioMenu.menus = {} 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 --]]-- + function radioMenu.addRadioMenu(theZone) table.insert(radioMenu.menus, theZone) end @@ -35,44 +45,192 @@ end -- -- read zone -- -function radioMenu.installMenu(theZone) - if theZone.coalition == 0 then - theZone.rootMenu = missionCommands.addSubMenu(theZone.rootName, nil) +function radioMenu.filterPlayerIDForType(theZone) + -- note: we currently ignore coalition + local theIDs = {} + local allTypes = {} + if dcsCommon.containsString(theZone.menuTypes, ",") then + allTypes = dcsCommon.splitString(theZone.menuTypes, ",") else - theZone.rootMenu = missionCommands.addSubMenuForCoalition(theZone.coalition, theZone.rootName, nil) + table.insert(allTypes, theZone.menuTypes) end - local menuA = cfxZones.getStringFromZoneProperty(theZone, "itemA", "") - if theZone.coalition == 0 then - theZone.menuA = missionCommands.addCommand(menuA, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "A"}) + -- 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 + + for idx, aType in pairs(allTypes) do + local theType = dcsCommon.trim(aType) + local lowerType = string.lower(theType) + + for gName, gData in pairs(cfxMX.playerGroupByName) do + -- get coalition of group + local coa = cfxMX.groupCoalitionByName[gName] + if (theZone.coalition == 0 or theZone.coalition == coa) then + -- do special types first + if dcsCommon.stringStartsWith(lowerType, "helo") or dcsCommon.stringStartsWith(lowerType, "heli") then + -- we look for all helicoperts + if cfxMX.groupTypeByName[gName] == "helicopter" then + theIDs[gName] = gData.groupId + if theZone.verbose or radioMenu.verbose then + trigger.action.outText("+++menu: Player Group <" .. gName .. "> matches gen-type helicopter", 30) + end + end + elseif lowerType == "plane" or lowerType == "planes" then + -- we look for all planes + if cfxMX.groupTypeByName[gName] == "plane" then + theIDs[gName] = gData.groupId + if theZone.verbose or radioMenu.verbose then + trigger.action.outText("+++menu: Player Group <" .. gName .. "> matches gen-type plane", 30) + end + end + else + -- we are looking for a particular type, e.g. A-10A + -- since groups do not carry the type, but all player + -- groups are of the same type, we access the first + -- unit. Note that this may later break if ED implement + -- player groups of mixed type + if gData.units and gData.units[1] and gData.units[1].type == theType then + theIDs[gName] = gData.groupId + if theZone.verbose or radioMenu.verbose then + trigger.action.outText("+++menu: Player Group <" .. gName .. "> matches type <" .. theType .. ">", 30) + end + else + + end + end + else + if theZone.verbose or radioMenu.verbose then + trigger.action.outText("+++menu: type check failed coalition for <" .. gName .. ">", 30) + end + end + end + end + return theIDs +end + +function radioMenu.filterPlayerIDForGroup(theZone) + -- create an iterable list of groups, separated by commas + -- note that we could introduce wildcards for groups later + local theIDs = {} + local allGroups = {} + if dcsCommon.containsString(theZone.menuGroup, ",") then + allGroups = dcsCommon.splitString(theZone.menuGroup, ",") else - theZone.menuA = missionCommands.addCommandForCoalition(theZone.coalition, menuA, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "A"}) + table.insert(allGroups, theZone.menuGroup) + end + + for idx, gName in pairs(allGroups) do + gName = dcsCommon.trim(gName) + local theGroup = cfxMX.playerGroupByName[gName] + if theGroup then + local gID = theGroup.groupId + table.insert(theIDs, gID) + if theZone.verbose or radioMenu.verbose then + trigger.action.outText("+++menu: Player Group <" .. gName .. "> found: <" .. gID .. ">", 30) + end + else + trigger.action.outText("+++menu: Player Group <" .. gName .. "> does not exist", 30) + end + end + + return theIDs +end + +function radioMenu.installMenu(theZone) +-- local theGroup = 0 -- was: nil + local gID = nil + if theZone.menuGroup then + if not cfxMX then + trigger.action.outText("WARNING: radioMenu's group attribute requires the 'cfxMX' module", 30) + return + end + -- access cfxMX player info for group ID + gID = radioMenu.filterPlayerIDForGroup(theZone) + elseif theZone.menuTypes then + if not cfxMX then + trigger.action.outText("WARNING: radioMenu's type attribute requires the 'cfxMX' module", 30) + return + end + -- access cxfMX player infor with type match for ID + gID = radioMenu.filterPlayerIDForType(theZone) + end + + theZone.rootMenu = {} + theZone.mcdA = {} + theZone.mcdB = {} + theZone.mcdC = {} + theZone.mcdD = {} + theZone.mcdA[0] = 0 + theZone.mcdB[0] = 0 + theZone.mcdC[0] = 0 + theZone.mcdD[0] = 0 + + if theZone.menuGroup or theZone.menuTypes then + for idx, grp in pairs(gID) do + local aRoot = missionCommands.addSubMenuForGroup(grp, theZone.rootName, nil) + theZone.rootMenu[grp] = aRoot + theZone.mcdA[grp] = 0 + theZone.mcdB[grp] = 0 + theZone.mcdC[grp] = 0 + theZone.mcdD[grp] = 0 + end + elseif theZone.coalition == 0 then + theZone.rootMenu[0] = missionCommands.addSubMenu(theZone.rootName, nil) + else + theZone.rootMenu[0] = missionCommands.addSubMenuForCoalition(theZone.coalition, theZone.rootName, nil) + end + + if cfxZones.hasProperty(theZone, "itemA") then + local menuA = cfxZones.getStringFromZoneProperty(theZone, "itemA", "") + if theZone.menuGroup or theZone.menuTypes then + theZone.menuA = {} + for idx, grp in pairs(gID) do + theZone.menuA[grp] = missionCommands.addCommandForGroup(grp, menuA, theZone.rootMenu[grp], radioMenu.redirectMenuX, {theZone, "A", grp}) + end + elseif theZone.coalition == 0 then + theZone.menuA = missionCommands.addCommand(menuA, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "A"}) + else + theZone.menuA = missionCommands.addCommandForCoalition(theZone.coalition, menuA, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "A"}) + end end if cfxZones.hasProperty(theZone, "itemB") then local menuB = cfxZones.getStringFromZoneProperty(theZone, "itemB", "") - if theZone.coalition == 0 then - theZone.menuB = missionCommands.addCommand(menuB, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "B"}) + if theZone.menuGroup or theZone.menuTypes then + for idx, grp in pairs(gID) do + theZone.menuB[grp] = missionCommands.addCommandForGroup(grp, menuB, theZone.rootMenu[grp], radioMenu.redirectMenuX, {theZone, "B"}) + end + elseif theZone.coalition == 0 then + theZone.menuB = missionCommands.addCommand(menuB, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "B"}) else - theZone.menuB = missionCommands.addCommandForCoalition(theZone.coalition, menuB, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "B"}) + theZone.menuB = missionCommands.addCommandForCoalition(theZone.coalition, menuB, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "B"}) end end if cfxZones.hasProperty(theZone, "itemC") then local menuC = cfxZones.getStringFromZoneProperty(theZone, "itemC", "") - if theZone.coalition == 0 then - theZone.menuC = missionCommands.addCommand(menuC, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "C"}) + if theZone.menuGroup or theZone.menuTypes then + for idx, grp in pairs(gID) do + theZone.menuC[grp] = missionCommands.addCommandForGroup(grp, menuC, theZone.rootMenu[grp], radioMenu.redirectMenuX, {theZone, "C"}) + end + elseif theZone.coalition == 0 then + theZone.menuC = missionCommands.addCommand(menuC, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "C"}) else - theZone.menuC = missionCommands.addCommandForCoalition(theZone.coalition, menuC, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "C"}) + theZone.menuC = missionCommands.addCommandForCoalition(theZone.coalition, menuC, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "C"}) end end if cfxZones.hasProperty(theZone, "itemD") then local menuD = cfxZones.getStringFromZoneProperty(theZone, "itemD", "") - if theZone.coalition == 0 then - theZone.menuD = missionCommands.addCommand(menuD, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "D"}) + if theZone.menuGroup or theZone.menuTypes then + for idx, grp in pairs(gID) do + theZone.menuD[grp] = missionCommands.addCommandForGroup(grp, menuD, theZone.rootMenu[grp], radioMenu.redirectMenuX, {theZone, "D"}) + end + elseif theZone.coalition == 0 then + theZone.menuD = missionCommands.addCommand(menuD, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "D"}) else - theZone.menuD = missionCommands.addCommandForCoalition(theZone.coalition, menuD, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "D"}) + theZone.menuD = missionCommands.addCommandForCoalition(theZone.coalition, menuD, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "D"}) end end end @@ -81,6 +239,18 @@ function radioMenu.createRadioMenuWithZone(theZone) theZone.rootName = cfxZones.getStringFromZoneProperty(theZone, "radioMenu", "") theZone.coalition = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0) + -- groups / types + if cfxZones.hasProperty(theZone, "group") then + theZone.menuGroup = cfxZones.getStringFromZoneProperty(theZone, "group", "") + theZone.menuGroup = dcsCommon.trim(theZone.menuGroup) + elseif cfxZones.hasProperty(theZone, "groups") then + theZone.menuGroup = cfxZones.getStringFromZoneProperty(theZone, "groups", "") + theZone.menuGroup = dcsCommon.trim(theZone.menuGroup) + elseif cfxZones.hasProperty(theZone, "type") then + theZone.menuTypes = cfxZones.getStringFromZoneProperty(theZone, "type", "none") + elseif cfxZones.hasProperty(theZone, "types") then + theZone.menuTypes = cfxZones.getStringFromZoneProperty(theZone, "types", "none") + end theZone.menuVisible = cfxZones.getBoolFromZoneProperty(theZone, "menuVisible", true) @@ -99,22 +269,22 @@ function radioMenu.createRadioMenuWithZone(theZone) theZone.itemAChosen = cfxZones.getStringFromZoneProperty(theZone, "A!", "*") theZone.cooldownA = cfxZones.getNumberFromZoneProperty(theZone, "cooldownA", 0) - theZone.mcdA = 0 + --theZone.mcdA = 0 theZone.busyA = cfxZones.getStringFromZoneProperty(theZone, "busyA", "Please stand by ( seconds)") theZone.itemBChosen = cfxZones.getStringFromZoneProperty(theZone, "B!", "*") theZone.cooldownB = cfxZones.getNumberFromZoneProperty(theZone, "cooldownB", 0) - theZone.mcdB = 0 + --theZone.mcdB = 0 theZone.busyB = cfxZones.getStringFromZoneProperty(theZone, "busyB", "Please stand by ( seconds)") theZone.itemCChosen = cfxZones.getStringFromZoneProperty(theZone, "C!", "*") theZone.cooldownC = cfxZones.getNumberFromZoneProperty(theZone, "cooldownC", 0) - theZone.mcdC = 0 + --theZone.mcdC = 0 theZone.busyC = cfxZones.getStringFromZoneProperty(theZone, "busyC", "Please stand by ( seconds)") theZone.itemDChosen = cfxZones.getStringFromZoneProperty(theZone, "D!", "*") theZone.cooldownD = cfxZones.getNumberFromZoneProperty(theZone, "cooldownD", 0) - theZone.mcdD = 0 + --theZone.mcdD = 0 theZone.busyD = cfxZones.getStringFromZoneProperty(theZone, "busyD", "Please stand by ( seconds)") if cfxZones.hasProperty(theZone, "removeMenu?") then @@ -160,24 +330,43 @@ function radioMenu.redirectMenuX(args) timer.scheduleFunction(radioMenu.doMenuX, args, timer.getTime() + 0.1) end +function radioMenu.cdByGID(cd, theZone, gID) + if not gID then gID = 0 end + --if not gID then return cd[0] end + return cd[gID] +end + +function radioMenu.setCDByGID(cd, theZone, gID, newVal) + if not gID then gID = 0 end + --theZone[cd] = newVal + -- + --end + local allCD = theZone[cd] + allCD[gID] = newVal + theZone[cd] = allCD +end + function radioMenu.doMenuX(args) theZone = args[1] theItemIndex = args[2] -- A, B , C .. ? - local cd = theZone.mcdA + theGroup = args[3] -- can be nil or groupID + if not theGroup then theGroup = 0 end + + local cd = radioMenu.cdByGID(theZone.mcdA, theZone, theGroup) --theZone.mcdA local busy = theZone.busyA local theFlag = theZone.itemAChosen -- decode A..X if theItemIndex == "B"then - cd = theZone.mcdB + cd = radioMenu.cdByGID(theZone.mcdB, theZone, theGroup) -- theZone.mcdB busy = theZone.busyB theFlag = theZone.itemBChosen elseif theItemIndex == "C" then - cd = theZone.mcdC + cd = radioMenu.cdByGID(theZone.mcdC, theZone, theGroup) -- theZone.mcdC busy = theZone.busyC theFlag = theZone.itemCChosen elseif theItemIndex == "D" then - cd = theZone.mcdD + cd = radioMenu.cdByGID(theZone.mcdD, theZone, theGroup) -- theZone.mcdD busy = theZone.busyD theFlag = theZone.itemDChosen end @@ -193,13 +382,13 @@ function radioMenu.doMenuX(args) -- set new cooldown -- needs own decoder A..X if theItemIndex == "A" then - theZone.mcdA = now + theZone.cooldownA + radioMenu.setCDByGID("mcdA", theZone, theGroup, now + theZone.cooldownA) elseif theItemIndex == "B" then - theZone.mcdB = now + theZone.cooldownB + radioMenu.setCDByGID("mcdB", theZone, theGroup, now + theZone.cooldownB) elseif theItemIndex == "C" then - theZone.mcdC = now + theZone.cooldownC + radioMenu.setCDByGID("mcdC", theZone, theGroup, now + theZone.cooldownC) else - theZone.mcdD = now + theZone.cooldownD + radioMenu.setCDByGID("mcdC", theZone, theGroup, now + theZone.cooldownC) end cfxZones.pollFlag(theFlag, theZone.radioMethod, theZone) @@ -208,10 +397,10 @@ function radioMenu.doMenuX(args) end end + -- -- Update -- required when we can enable/disable a zone's menu -- - function radioMenu.update() -- call me in a second to poll triggers timer.scheduleFunction(radioMenu.update, {}, timer.getTime() + 1/radioMenu.ups) @@ -221,15 +410,15 @@ function radioMenu.update() if theZone.removeMenu and cfxZones.testZoneFlag(theZone, theZone.removeMenu, theZone.radioTriggerMethod, "lastRemoveMenu") and theZone.menuVisible - then - if theZone.verbose or radioMenu.verbose then - trigger.action.outText("+++menu: removing <" .. dcsCommon.menu2text(theZone.rootMenu) .. "> for <" .. theZone.name .. ">", 30) - end - - if theZone.coalition == 0 then - missionCommands.removeItem(theZone.rootMenu) + then + if theZone.menuGroup or theZone.menuTypes then + for gID, aRoot in pairs(theZone.rootMenu) do + missionCommands.removeItemForGroup(gID, aRoot) + end + elseif theZone.coalition == 0 then + missionCommands.removeItem(theZone.rootMenu[0]) else - missionCommands.removeItemForCoalition(theZone.coalition, theZone.rootMenu) + missionCommands.removeItemForCoalition(theZone.coalition, theZone.rootMenu[0]) end theZone.menuVisible = false @@ -304,5 +493,6 @@ if not radioMenu.start() then end --[[-- - callbacks for the menus + callbacks for the menus + check CD/standby code for multiple groups --]]-- \ No newline at end of file diff --git a/modules/sequencer.lua b/modules/sequencer.lua new file mode 100644 index 0000000..418140f --- /dev/null +++ b/modules/sequencer.lua @@ -0,0 +1,440 @@ +sequencer = {} +sequencer.version = "1.0.0" +sequencer.verbose = false +sequencer.requiredLibs = { + "dcsCommon", -- always + "cfxZones", -- Zones, of course +} +--[[-- + Sequencer: pull flags in a sequence with oodles of features + + Copyright (c) 2022 by Christian Franz +--]]-- + +sequencer.sequencers = {} + +function sequencer.addSequencer(theZone) + if not theZone then return end + table.insert(sequencer.sequencers, theZone) +end + +function sequencer.getSequenceByName(aName) + if not aName then return nil end + for idx, aZone in pairs(sequencer.sequencers) do + if aZone.name == aName then return aZone end + end + return nil +end + +-- +-- read from ME +-- + +function sequencer.createSequenceWithZone(theZone) + local seqRaw = cfxZones.getStringFromZoneProperty(theZone, "sequence!", "none") + local theFlags = dcsCommon.flagArrayFromString(seqRaw) + theZone.sequence = theFlags + local interRaw = cfxZones.getStringFromZoneProperty(theZone, "intervals", "86400") + if cfxZones.hasProperty(theZone, "interval") then + interRaw = cfxZones.getStringFromZoneProperty(theZone, "interval", "86400") -- = 24 * 3600 = 24 hours default interval + end + + local theIntervals = dcsCommon.rangeArrayFromString(interRaw, false) + theZone.intervals = theIntervals + + theZone.seqIndex = 1 -- we start at one + theZone.intervalIndex = 1 -- here too + + theZone.onStart = cfxZones.getBoolFromZoneProperty(theZone, "onStart", false) + theZone.zeroSequence = cfxZones.getBoolFromZoneProperty(theZone, "zeroSequence", true) + + theZone.seqLoop = cfxZones.getBoolFromZoneProperty(theZone, "loop", false) + + theZone.seqRunning = false + theZone.seqComplete = false + theZone.seqStarted = false + + theZone.timeLimit = 0 -- will be set to when we expire + if cfxZones.hasProperty(theZone, "done!") then + theZone.seqDone = cfxZones.getStringFromZoneProperty(theZone, "done!", "") + elseif cfxZones.hasProperty(theZone, "seqDone!") then + theZone.seqDone = cfxZones.getStringFromZoneProperty(theZone, "seqDone!", "") + end + + if cfxZones.hasProperty(theZone, "next?") then + theZone.nextSeq = cfxZones.getStringFromZoneProperty(theZone, "next?", "") + theZone.lastNextSeq = cfxZones.getFlagValue(theZone.nextSeq, theZone) + end + + if cfxZones.hasProperty(theZone, "startSeq?") then + theZone.startSeq = cfxZones.getStringFromZoneProperty(theZone, "startSeq?", "") + theZone.lastStartSeq = cfxZones.getFlagValue(theZone.startSeq, theZone) + --trigger.action.outText("read as " .. theZone.startSeq, 30) + end + + if cfxZones.hasProperty(theZone, "stopSeq?") then + theZone.stopSeq = cfxZones.getStringFromZoneProperty(theZone, "stopSeq?", "") + theZone.lastStopSeq = cfxZones.getFlagValue(theZone.stopSeq, theZone) + end + + if cfxZones.hasProperty(theZone, "resetSeq?") then + theZone.resetSeq = cfxZones.getStringFromZoneProperty(theZone, "resetSeq?", "") + theZone.lastResetSeq = cfxZones.getFlagValue(theZone.resetSeq, theZone) + end + + + -- methods + theZone.seqMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") + if cfxZones.hasProperty(theZone, "seqMethod") then + theZone.seqMethod = cfxZones.getStringFromZoneProperty(theZone, "seqMethod", "inc") + end + + theZone.seqTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") + if cfxZones.hasProperty(theZone, "seqTriggerMethod") then + theZone.seqTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "seqTriggerMethod", "change") + end + + if (not theZone.onStart) and not (theZone.startSeq) then + trigger.action.outText("+++seq: WARNING - sequence <" .. theZone.name .. "> cannot be started: no startSeq? and onStart is false", 30) + end +end + +function sequencer.fire(theZone) + -- time's up. poll flag at index + local theFlag = theZone.sequence[theZone.seqIndex] + if theFlag then + cfxZones.pollFlag(theFlag, theZone.seqMethod, theZone) + if theZone.verbose or sequencer.verbose then + trigger.action.outText("+++seq: triggering flag <" .. theFlag .. "> for index <" .. theZone.seqIndex .. "> in sequence <" .. theZone.name .. ">", 30) + end + else + trigger.action.outText("+++seq: ran out of sequences for <" .. theZone.name .. "> on index <" .. theZone.seqIndex .. ">", 30) + end +end + +function sequencer.advanceInterval(theZone) + theZone.intervalIndex = theZone.intervalIndex + 1 + if theZone.intervalIndex > #theZone.intervals then + theZone.intervalIndex = 1 -- always loops + end +end + +function sequencer.advanceSeq(theZone) + -- get the next index for the sequence + theZone.seqIndex = theZone.seqIndex + 1 + + -- loop if over and enabled + if theZone.seqIndex > #theZone.sequence then + if theZone.seqLoop then + theZone.seqIndex = 1 + else + return false + end + end + -- returns true if success + return true +end + +function sequencer.startWaitCycle(theZone) + if theZone.seqComplete then return end + local bounds = theZone.intervals[theZone.intervalIndex] + local newInterval = dcsCommon.randomBetween(bounds[1], bounds[2]) + theZone.timeLimit = timer.getTime() + newInterval + if theZone.verbose or sequencer.verbose then + trigger.action.outText("+++seq: start wait for <" .. newInterval .. "> in sequence <" .. theZone.name .. ">", 30) + end +end + +function sequencer.pause(theZone) + if theZone.seqComplete then return end + if not theZone.seqRunning then return end + local now = timer.getTime() + theZone.timeRemaining = theZone.timeLimit - now + theZone.seqRunning = false +end + +function sequencer.continue(theZone) + if theZone.seqComplete then return end -- Frankie says: no more + if theZone.seqRunning then return end -- we are already running + + -- reset any lingering 'next' flags so they don't + -- trigger a newly started sequence + if theZone.nextSeq then + theZone.lastNextSeq = cfxZones.getFlagValue(theZone.nextSeq, theZone) + end + + if not theZone.seqStarted then + -- this is the very first time we are running. + if theZone.zeroSequence then + -- start with a bang + sequencer.fire(theZone) + sequencer.advanceSeq(theZone) + end + theZone.seqRunning = true + theZone.seqStarted = true + sequencer.startWaitCycle(theZone) + return + end + + -- we are continuing a paused sequencer + local now = timer.getTime() + if not theZone.timeRemaining then theZone.timeRemaining = 1 end + theZone.timeLimit = now + theZone.timeRemaining + theZone.seqRunning = true +end + +function sequencer.reset(theZone) + theZone.seqComplete = false + theZone.seqRunning = false + theZone.seqIndex = 1 -- we start at one + theZone.intervalIndex = 1 -- here too + theZone.seqStarted = false + if theZone.onStart then + theZone.continue(theZone) + end +end + +--- +--- update +--- +function sequencer.update() + -- call me in a second to poll triggers + local now = timer.getTime() + timer.scheduleFunction(sequencer.update, {}, now + 1) + + for idx, theZone in pairs(sequencer.sequencers) do + -- see if reset was pulled + if theZone.resetSeq and cfxZones.testZoneFlag(theZone, theZone.resetSeq, theZone.seqTriggerMethod, "lastResetSeq") then + sequencer.reset(theZone) + end + + --trigger.action.outText("have as " .. theZone.startSeq, 30) + -- first, check if we need to pause or continue + if (not theZone.seqRunning) and theZone.startSeq and + cfxZones.testZoneFlag(theZone, theZone.startSeq, theZone.seqTriggerMethod, "lastStartSeq") then + sequencer.continue(theZone) + if theZone.verbose or sequencer.verbose then + trigger.action.outText("+++seq: continuing sequencer <" .. theZone.name .. ">", 30) + end + else + -- synch the start flag so we don't immediately trigger + -- when it starts + if theZone.startSeq then + theZone.lastStartSeq = cfxZones.getFlagValue(theZone.startSeq, theZone) + end + end + + if theZone.seqRunning and theZone.stopSeq and + cfxZones.testZoneFlag(theZone, theZone.stopSeq, theZone.seqTriggerMethod, "lastStopSeq") then + sequencer.pause(theZone) + if theZone.verbose or sequencer.verbose then + trigger.action.outText("+++seq: pausing sequencer <" .. theZone.name .. ">", 30) + end + else + if theZone.stopSeq then + theZone.lastStopSeq = cfxZones.getFlagValue(theZone.stopSeq, theZone) + end + end + + -- if we are running, see if we timed out + if theZone.seqRunning then + -- check if we have received a 'next' signal + local doNext = false + if theZone.nextSeq then + doNext = cfxZones.testZoneFlag(theZone, theZone.nextSeq, theZone.seqTriggerMethod, "lastNextSeq") + if doNext and (sequencer.verbose or theZone.verbose) then + trigger.action.outText("+++seq: 'next' command received for sequencer <" .. theZone.name .. "> on <" .. theZone.nextSeq .. ">", 30) + end + end + + -- check if we are over time limit + if doNext or (theZone.timeLimit < now) then + -- we are timed out or triggered! + if theZone.nextSeq then + theZone.lastNextSeq = cfxZones.getFlagValue(theZone.nextSeq, theZone) + end + sequencer.fire(theZone) + sequencer.advanceInterval(theZone) + if sequencer.advanceSeq(theZone) then + -- start next round + sequencer.startWaitCycle(theZone) + else + if theZone.seqDone then + cfxZones.pollFlag(theZone.seqDone, theZone.seqMethod, theZone) + if theZone.verbose or sequencer.verbose then + trigger.action.outText("+++seq: banging done! flag <" .. theZone.seqDone .. "> for sequence <" .. theZone.name .. ">", 30) + end + end + theZone.seqRunning = false + theZone.seqComplete = true -- can't be restarted unless reset + end -- else no advance + end -- if time limit + end -- if running + end -- for all sequencers +end + +-- +-- start cycle: force all onStart to fire +-- +function sequencer.startCycle() + for idx, theZone in pairs(sequencer.sequencers) do + -- a sequence can be already running when persistence + -- loaded a sequencer + if theZone.onStart then + if theZone.seqStarted then + -- suppressed by persistence + else + if sequencer.verbose or theZone.verbose then + trigger.action.outText("+++seq: starting sequencer " .. theZone.name, 30) + end + sequencer.continue(theZone) + end + end + end +end + +-- +-- LOAD / SAVE +-- +function sequencer.saveData() + local theData = {} + local allSequencers = {} + local now = timer.getTime() + for idx, theSeq in pairs(sequencer.sequencers) do + local theName = theSeq.name + local seqData = {} + seqData.seqComplete = theSeq.seqComplete + seqData.seqRunning = theSeq.seqRunning + seqData.seqIndex = theSeq.seqIndex + seqData.intervalIndex = theSeq.intervalIndex + seqData.seqStarted = theSeq.seqStarted + seqData.timeRemaining = theSeq.timeRemaining + if theSeq.seqRunning then + seqData.timeRemaining = theSeq.timeLimit - now + end + + allSequencers[theName] = seqData + end + theData.allSequencers = allSequencers + return theData +end + +function sequencer.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("sequencer") + if not theData then + if sequencer.verbose then + trigger.action.outText("+++seq Persistence: no save date received, skipping.", 30) + end + return + end + + local allSequencers = theData.allSequencers + if not allSequencers then + if sequencer.verbose then + trigger.action.outText("+++seq Persistence: no sequencer data, skipping", 30) + end + return + end + + local now = timer.getTime() + for theName, seqData in pairs(allSequencers) do + local theSeq = sequencer.getSequenceByName(theName) + if theSeq then + theSeq.seqComplete = seqData.seqComplete + theSeq.seqIndex = seqData.seqIndex + theSeq.intervalIndex = seqData.intervalIndex + theSeq.seqStarted = seqData.seqStarted + theSeq.seqRunning = seqData.seqRunning + theSeq.timeRemaining = seqData.timeRemaining + if theSeq.seqRunning then + theSeq.timeLimit = now + theSeq.timeRemaining + end + + else + trigger.action.outText("+++seq: persistence: cannot synch sequencer <" .. theName .. ">, skipping", 40) + end + end +end + +-- +-- start module and read config +-- +function sequencer.readConfigZone() + -- note: must match exactly!!!! + local theZone = cfxZones.getZoneByName("sequencerConfig") + if not theZone then + theZone = cfxZones.createSimpleZone("sequencerConfig") + if sequencer.verbose then + trigger.action.outText("***RND: NO config zone!", 30) + end + end + + sequencer.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + + if sequencer.verbose then + trigger.action.outText("***RND: read config", 30) + end +end + +function sequencer.start() + -- lib check + if not dcsCommon then + trigger.action.outText("sequencer requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx Sequencer", + sequencer.requiredLibs) then + return false + end + + -- read config + sequencer.readConfigZone() + + -- process RND Zones + local attrZones = cfxZones.getZonesWithAttributeNamed("sequence!") + + if sequencer.verbose then + local a = dcsCommon.getSizeOfTable(attrZones) + trigger.action.outText("sequencers: " .. a, 30) + end + + -- now create an rnd gen for each one and add them + -- to our watchlist + for k, aZone in pairs(attrZones) do + sequencer.createSequenceWithZone(aZone) -- process attribute and add to zone + sequencer.addSequencer(aZone) -- remember it so we can smoke it + end + + + -- persistence + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = sequencer.saveData + persistence.registerModule("sequencer", callbacks) + -- now load my data + sequencer.loadData() + end + + -- schedule start cycle + timer.scheduleFunction(sequencer.startCycle, {}, timer.getTime() + 0.25) + + -- start update + timer.scheduleFunction(sequencer.update, {}, timer.getTime() + 1) + + trigger.action.outText("cfx Sequencer v" .. sequencer.version .. " started.", 30) + return true +end + +-- let's go! +if not sequencer.start() then + trigger.action.outText("cf/x Sequencer aborted: missing libraries", 30) + sequencer = nil +end + +--[[-- + to do: + - currSeq always returns current sequence number + - timeLeft returns current time limit in seconds +--]]-- \ No newline at end of file diff --git a/tutorial & demo missions/demo - Attack of the CloneZ.miz b/tutorial & demo missions/demo - Attack of the CloneZ.miz index ae646c1..5266c68 100644 Binary files a/tutorial & demo missions/demo - Attack of the CloneZ.miz and b/tutorial & demo missions/demo - Attack of the CloneZ.miz differ diff --git a/tutorial & demo missions/demo - Reinforcements A La Carte.miz b/tutorial & demo missions/demo - Reinforcements A La Carte.miz index d65a2a1..5a88196 100644 Binary files a/tutorial & demo missions/demo - Reinforcements A La Carte.miz and b/tutorial & demo missions/demo - Reinforcements A La Carte.miz differ diff --git a/tutorial & demo missions/demo - The Zonal Countdown.miz b/tutorial & demo missions/demo - The Zonal Countdown.miz index 6e771b3..57d23d9 100644 Binary files a/tutorial & demo missions/demo - The Zonal Countdown.miz and b/tutorial & demo missions/demo - The Zonal Countdown.miz differ diff --git a/tutorial & demo missions/demo - bottled messages.miz b/tutorial & demo missions/demo - bottled messages.miz index 33fb871..3cc6191 100644 Binary files a/tutorial & demo missions/demo - bottled messages.miz and b/tutorial & demo missions/demo - bottled messages.miz differ diff --git a/tutorial & demo missions/demo - recon mode - reloaded.miz b/tutorial & demo missions/demo - recon mode - reloaded.miz index 277b821..86af1c5 100644 Binary files a/tutorial & demo missions/demo - recon mode - reloaded.miz and b/tutorial & demo missions/demo - recon mode - reloaded.miz differ diff --git a/tutorial & demo missions/demo - track this!.miz b/tutorial & demo missions/demo - track this!.miz index 03304c8..9d463c2 100644 Binary files a/tutorial & demo missions/demo - track this!.miz and b/tutorial & demo missions/demo - track this!.miz differ