diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 68abda0..61eb264 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 dfceb0e..6919fa3 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/cargoReceiver.lua b/modules/cargoReceiver.lua index b54e49f..ff6ec3e 100644 --- a/modules/cargoReceiver.lua +++ b/modules/cargoReceiver.lua @@ -1,5 +1,5 @@ cfxCargoReceiver = {} -cfxCargoReceiver.version = "2.0.0" +cfxCargoReceiver.version = "2.1.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 = { @@ -12,7 +12,10 @@ cfxCargoReceiver.requiredLibs = { - 2.0.0 no more cfxPlayer Dependency dmlZones, OOP clean-up - + - 2.1.0 code maintenance + cargo gets removed from mgr when touching ground + switch to cargoReceiver! + noBounce attribute CargoReceiver is a zone enhancement you use to be automatically notified if a cargo was delivered inside the zone. @@ -25,26 +28,30 @@ cfxCargoReceiver.receiverZones = {} function cfxCargoReceiver.processReceiverZone(aZone) -- process attribute and add to zone -- since the attribute is there, simply set the zones -- isCargoReceiver flag and we are good - aZone.isCargoReceiver = true - -- we can add additional processing here - aZone.autoRemove = aZone:getBoolFromZoneProperty("autoRemove", false) -- maybe add a removeDelay + aZone.isCargoReceiver = true -- so all zones can detect this. Necessary? + aZone.autoRemove = aZone:getBoolFromZoneProperty("autoRemove", false) aZone.removeDelay = aZone:getNumberFromZoneProperty("removeDelay", 1) if aZone.removeDelay < 1 then aZone.removeDelay = 1 end aZone.silent = aZone:getBoolFromZoneProperty("silent", false) - - - -- new method support aZone.cargoMethod = aZone:getStringFromZoneProperty("method", "inc") if aZone:hasProperty("cargoMethod") then aZone.cargoMethod = aZone:getStringFromZoneProperty("cargoMethod", "inc") end - if aZone:hasProperty("f!") then + -- save the names of cargos delivered to prevent 'bounce' + aZone.cargosDelivered = {} -- by cargo name + aZone.noBounce = aZone:getBoolFromZoneProperty("noBounce", true) + if aZone:hasProperty("cargoReceiver!") then + aZone.outReceiveFlag = aZone:getStringFromZoneProperty("cargoReceiver!", "*") + elseif aZone:hasProperty("f!") then aZone.outReceiveFlag = aZone:getStringFromZoneProperty("f!", "*") elseif aZone:hasProperty("cargoReceived!") then aZone.outReceiveFlag = aZone:getStringFromZoneProperty( "cargoReceived!", "*") end + if not aZone.outReceiveFlag then + trigger.action.outText("+++crgR: receiver <" .. aZone.name .. "> has no output!", 30) + end end function cfxCargoReceiver.addReceiverZone(aZone) @@ -94,7 +101,7 @@ function cfxCargoReceiver.cargoEvent(event, object, name) if not event then return end if event == "grounded" then - -- this is actually the only one that interests us + -- this is the only one that interests us if not object then return end @@ -106,21 +113,28 @@ function cfxCargoReceiver.cargoEvent(event, object, name) -- now invoke callbacks for all zones -- this is in for name, aZone in pairs(cfxCargoReceiver.receiverZones) do - if cfxZones.pointInZone(loc, aZone) then + if aZone:pointInZone(loc) then cfxCargoReceiver.invokeCallback("deliver", object, name, aZone) -- set flags as indicated if aZone.outReceiveFlag then - cfxZones.pollFlag(aZone.outReceiveFlag, aZone.cargoMethod, aZone) + if aZone.noBounce then + -- we only do this once for every named object + if not aZone.cargosDelivered[name] then + aZone.cargosDelivered[name] = true + aZone:pollFlag(aZone.outReceiveFlag, aZone.cargoMethod) + end + else + aZone:pollFlag(aZone.outReceiveFlag, aZone.cargoMethod) + end end if aZone.autoRemove then - -- schedule this for in a few seconds? + -- schedule this for in a few seconds local args = {} args.theObject = object args.theZone = aZone timer.scheduleFunction(cfxCargoReceiver.removeCargo, args, timer.getTime() + aZone.removeDelay) - --object:destroy() end end end @@ -139,17 +153,13 @@ function cfxCargoReceiver.update() -- new we see if any of these are close to a delivery zone for idx, aCargo in pairs(liftedCargos) do local thePoint = aCargo:getPoint() - local receiver = cfxZones.getClosestZone( - thePoint, - cfxCargoReceiver.receiverZones -- must be indexed by name - ) + local receiver = cfxZones.getClosestZone(thePoint, cfxCargoReceiver.receiverZones) -- must be indexed by name -- we now check if we are in 'speaking range' and receiver can talk - -- modify delta by distance to boundary, not - -- center + -- modify delta by distance to boundary, not center local delta = dcsCommon.distFlat(thePoint, cfxZones.getPoint(receiver)) delta = delta - receiver.radius - if (receiver.silent == false) and + if (not receiver.silent) and (delta < cfxCargoReceiver.maxDirectionRange) then -- this cargo can be talked down. -- find the player unit that is closest to in in hopes @@ -214,10 +224,14 @@ function cfxCargoReceiver.start() end -- scan all zones for cargoReceiver flag + local attrZones = cfxZones.getZonesWithAttributeNamed("cargoReceiver!") + for k, aZone in pairs(attrZones) do + cfxCargoReceiver.processReceiverZone(aZone) + cfxCargoReceiver.addReceiverZone(aZone) + end + + -- old version, LEGACY local attrZones = cfxZones.getZonesWithAttributeNamed("cargoReceiver") - - -- 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 cfxCargoReceiver.processReceiverZone(aZone) -- process attribute and add to zone cfxCargoReceiver.addReceiverZone(aZone) -- remember it so we can smoke it diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index 20643b0..7969745 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "4.4.2" +cfxZones.version = "4.4.3" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -32,6 +32,7 @@ cfxZones.version = "4.4.2" - dmlZone:getCoalition() dereferences masterOwner once -4.4.1 - better verbosity for error in doPollFlag() -4.4.2 - twn support for wildcards and +-4.4.3 - property name is trimmed (double check) --]]-- -- @@ -123,7 +124,14 @@ function cfxZones.readFromDCS(clearfirst) else newZone.properties = {} end -- WARNING: REF COPY. May need to clone - +--[[-- + trigger.action.outText("zone <> properties:trimmed", 30) + local msg = "[" + for idx, val in pairs(newZone.properties) do + msg = msg .. "<" .. val.key .. ">:<" .. dcsCommon.trim(val.key) .. ">, " + end + trigger.action.outText(msg, 30) +--]]-- local upperName = newZone.name:upper() -- location as 'point' @@ -2158,7 +2166,14 @@ function cfxZones.extractPropertyFromDCS(theKey, theProperties) for i=1, #theProperties do local theP = theProperties[i] - local existingKey = dcsCommon.trim(theP.key) + local existingKey = dcsCommon.trim(theP.key) -- does not work if ends on "#!?" - why? + --while len(existingKey) > 1 and string.sub(existingKey, -1) == " " do + -- existingKey = existingKey:sub(1, -2) -- trim manually + --end + if string.sub(existingKey, -1) == " " then + trigger.action.outText("+++ZONES: warning: unable to trim blanks from attribute name <" .. existingKey .. ">", 30) + end +-- trigger.action.outText("procced attribute name <" .. existingKey .. ">", 30) if not cfxZones.caseSensitiveProperties then existingKey = string.lower(existingKey) end @@ -2388,6 +2403,7 @@ function dmlZone:hasProperty(theProperty) trigger.action.outText("+++zne: WARNING - hasProperty called with nil theProperty for zone <" .. self.name .. ">", 30) return false end + theProperty = dcsCommon.trim(theProperty) -- strip leading and training blanks local foundIt = self:getZoneProperty(theProperty) if not foundIt then -- check for possible forgotten or exchanged IO flags diff --git a/modules/civAir.lua b/modules/civAir.lua index d4b0a63..8f2cf26 100644 --- a/modules/civAir.lua +++ b/modules/civAir.lua @@ -1,5 +1,5 @@ civAir = {} -civAir.version = "3.0.2" +civAir.version = "3.1.0" --[[-- 3.0.0 liveries support default liveries for Yak-50 (main test case) @@ -15,7 +15,12 @@ civAir.version = "3.0.2" protest action spawning now works correctly for groupType 3.0.2 clean-up - + 3.1.0 better verbosity for startup info + dicts for departOnly and landingOnly + correctly filter depart only, land only, closed at start + Syria multiple "H" airfields DCS bug + filter airfields with no runways + getAirfieldNamed --]]-- civAir.ups = 0.05 -- updates per second. 0.05 = once every 20 seconds @@ -60,8 +65,11 @@ civAir.maxIdle = 8 * 60 -- seconds of ide time before it is removed after landin civAir.trafficCenters = {} civAir.excludeAirfields = {} +civAir.excludeAirfieldsDict = {} civAir.departOnly = {} -- use only to start from +civAir.departOnlyDict = {} -- as above, by af name civAir.landingOnly = {} -- use only to land at +civAir.landingOnlyDict = {} -- as above, by af name civAir.inoutZones = {} -- off-map connector zones civAir.requiredLibs = { @@ -180,7 +188,7 @@ end function civAir.processZone(theZone) local value = theZone:getStringFromZoneProperty("civAir", "") - local af = dcsCommon.getClosestAirbaseTo(theZone.point, 0) -- 0 = only airfields, not farp or ships + local af = dcsCommon.getClosestAirbaseTo(theZone.point, 0) -- 0 = only airfields, not farp nor ships local inoutName = "***" .. theZone:getName() if af then @@ -188,10 +196,13 @@ function civAir.processZone(theZone) value = value:lower() if value == "exclude" or value == "closed" then table.insert(civAir.excludeAirfields, afName) + civAir.excludeAirfieldsDict[afName] = afName elseif dcsCommon.stringStartsWith(value, "depart") or dcsCommon.stringStartsWith(value, "start") or dcsCommon.stringStartsWith(value, "take") then table.insert(civAir.departOnly, afName) + civAir.departOnlyDict[afName] = afName -- can fold many in one elseif dcsCommon.stringStartsWith(value, "land") or dcsCommon.stringStartsWith(value, "arriv") then table.insert(civAir.landingOnly, afName) + civAir.landingOnlyDict[afName] = afName -- can fold many on one elseif dcsCommon.stringStartsWith(value, "inb") then table.insert(civAir.departOnly, inoutName) -- start in inbound zone civAir.inoutZones[inoutName] = theZone @@ -253,8 +264,8 @@ function civAir.filterAirfields(inAll, inFilter) end function civAir.getTwoAirbases() - local fAB -- first airbase to depart - local sAB -- second airbase to fly to + local fAB -- name of first airbase to depart from + local sAB -- name second airbase to fly to local departAB = dcsCommon.combineTables(civAir.trafficCenters, civAir.departOnly) -- remove all currently excluded air bases from departure @@ -263,11 +274,14 @@ function civAir.getTwoAirbases() if #filteredAB < 1 then trigger.action.outText("+++civA: too few departure airfields", 30) return nil, nil + else +-- trigger.action.outText("+++civA: source (first) fields available: " .. #filteredAB, 30) end -- now pick the departure airfield - fAB = dcsCommon.pickRandom(filteredAB) - + fAB = dcsCommon.pickRandom(filteredAB) -- returns an airfield name +-- trigger.action.outText("+++civA: picked first: <" .. fAB .. ">", 30) + -- now generate list of landing airfields local arriveAB = dcsCommon.combineTables(civAir.trafficCenters, civAir.landingOnly) -- remove all currently excluded air bases from arrival @@ -277,6 +291,8 @@ function civAir.getTwoAirbases() if #filteredAB < 1 then trigger.action.outText("+++civA: too few arrival airfields", 30) return nil, nil + else +-- trigger.action.outText("+++civA: second (ARV) fields available: " .. #filteredAB, 30) end -- pick any second that are not the same @@ -285,10 +301,13 @@ function civAir.getTwoAirbases() sAB = dcsCommon.pickRandom(filteredAB) tries = tries + 1 -- only try 10 times until fAB ~= sAB or tries > 10 - + +-- trigger.action.outText("+++civA: picked SECOND: <" .. sAB .. ">", 30) + + local civA = {} if not (dcsCommon.stringStartsWith(fAB, '***')) then - civA.AB = dcsCommon.getFirstAirbaseWhoseNameContains(fAB, 0) + civA.AB = civAir.getAirfieldNamed(fAB) -- dcsCommon.getFirstAirbaseWhoseNameContains(fAB, 0) civA.name = civA.AB:getName() else civA.zone = civAir.inoutZones[fAB] @@ -296,16 +315,22 @@ function civAir.getTwoAirbases() end local civB = {} if not (dcsCommon.stringStartsWith(sAB, '***')) then - civB.AB = dcsCommon.getFirstAirbaseWhoseNameContains(sAB, 0) + civB.AB = civAir.getAirfieldNamed(sAB) -- dcsCommon.getFirstAirbaseWhoseNameContains(sAB, 0) civB.name = civB.AB:getName() else civB.zone = civAir.inoutZones[sAB] civB.name = civB.zone:getName() end +-- trigger.action.outText("+++civA: picked source AF <" .. civA.name .. "> and dest <" .. civB.name .. ">", 30) + return civA, civB -- fAB, sAB end +function civAir.getAirfieldNamed(name) + return Airbase.getByName(name) +end + function civAir.parkingIsFree(fromWP) -- iterate over all currently registred flights and make -- sure that their location isn't closer than 10m to my new parking @@ -684,24 +709,48 @@ function civAir.collectHubs() end function civAir.listTrafficCenters() - trigger.action.outText("Traffic Centers", 30) + local msg = "Traffic Centers:\n" + local other = false for idx, aName in pairs(civAir.trafficCenters) do - trigger.action.outText(aName, 30) + if other then msg = msg .. ", " end + msg = msg .. aName + other = true end + trigger.action.outText(msg .. "\n", 30) + msg = "Departure-Only:\n" + other = false if #civAir.departOnly > 0 then - trigger.action.outText("Departure-Only:", 30) for idx, aName in pairs(civAir.departOnly) do - trigger.action.outText(aName, 30) + if other then msg = msg .. ", " end + msg = msg .. aName + other = true end end + trigger.action.outText(msg .. "\n", 30) + msg = "Landing-Only:\n" + other = false if #civAir.landingOnly > 0 then - trigger.action.outText("Arrival/Landing-Only:", 30) for idx, aName in pairs(civAir.landingOnly) do - trigger.action.outText(aName, 30) + if other then msg = msg .. ", " end + msg = msg .. aName + other = true end end + trigger.action.outText(msg .. "\n", 30) + + + msg = "Closed:\n" + other = false + if #civAir.excludeAirfields > 0 then + for idx, aName in pairs(civAir.excludeAirfields) do + if other then msg = msg .. ", " end + msg = msg .. aName + other = true + end + end + trigger.action.outText(msg .. "\n", 30) end -- start @@ -721,13 +770,27 @@ function civAir.start() if (#civAir.trafficCenters + #civAir.departOnly < 1) or (#civAir.trafficCenters + #civAir.landingOnly < 1) then + trigger.action.outText("+++civA: marked traffic centers:" .. #civAir.trafficCenters .. "\n depart-only:" .. #civAir.departOnly .. "\n landing-only: " .. #civAir.landingOnly .. "\n", 30 ) trigger.action.outText("+++civA: auto-populating", 30) -- simply add airfields on the map + -- and filter those that are excluded, land-only or depart only local allBases = dcsCommon.getAirbasesWhoseNameContains("*", 0) for idx, aBase in pairs(allBases) do local afName = aBase:getName() - - table.insert(civAir.trafficCenters, afName) + if civAir.departOnlyDict[afName] or + civAir.landingOnlyDict[afName] or + civAir.excludeAirfieldsDict[afName] + then + trigger.action.outText("+++civA: filtering base " .. afName .. " for single-exception", 30) + else + -- syria special proccing + local rw = aBase:getRunways() + if #rw < 1 then +-- trigger.action.outText(afName .. " has no runways, filtered", 30) + else + table.insert(civAir.trafficCenters, afName) + end + end end end diff --git a/modules/convoy.lua b/modules/convoy.lua index 05f0e25..ba2e68c 100644 --- a/modules/convoy.lua +++ b/modules/convoy.lua @@ -1,5 +1,5 @@ convoy = {} -convoy.version = "1.1.0" +convoy.version = "1.2.0" convoy.requiredLibs = { "dcsCommon", "cfxZones", @@ -32,7 +32,8 @@ VERSION HISTORY - warning when not onStart and no start? - say hi - removed destination attribute - +1.2.0 - convoyMethod + - convoyTriggerMethod --]]-- --[[-- CONVOY Structure @@ -174,6 +175,9 @@ function convoy.readConvoyZone(theZone) trigger.action.outText("+++CVY: Warning: Convoy zone <" .. theZone.name .. "> has disabled 'onStart' and has no 'spawn?' input. This convoy zone can't send out any convoys,", 30) end end + theZone.convoyMethod = theZone:getStringFromZoneProperty("convoyMethod", "inc") + theZone.convoyTriggerMethod = theZone:getStringFromZoneProperty("convoyTriggerMethod", "change") + --[[-- if theZone:hasProperty("destination") then -- remove me theZone.destination = theZone:getStringFromZoneProperty("destination", "none") @@ -560,7 +564,7 @@ function convoy.wpReached(gName, convName, idx, wpNum) convoy.invokeArrivedCallbacks(theConvoy) -- hit the output flag if defined if theZone.arrivedOut then - theZone:pollFlag(theZone.arrivedOut, "inc") + theZone:pollFlag(theZone.arrivedOut, theZone.convoyMethod) -- "inc") end -- remove convoy from watchlist convoy.convoys[convName] = nil @@ -845,7 +849,7 @@ function convoy.update() -- check for flags for idx, theZone in pairs (convoy.zones) do if theZone.spawnFlag and - theZone:testZoneFlag(theZone.spawnFlag, "change", "lastSpawnFlag") then + theZone:testZoneFlag(theZone.spawnFlag, theZone.convoyTriggerMethod, "lastSpawnFlag") then convoy.startConvoy(theZone) end end @@ -900,7 +904,7 @@ function convoy.statusUpdate() -- every 10 seconds convoy.invokeAttackedCallbacks(theConvoy) theZone = theConvoy.origin if theZone.attackedOut then - theZone:pollFlag(theZone.attackedOut, "inc") + theZone:pollFlag(theZone.attackedOut, theZone.convoyMethod) -- "inc") end end @@ -909,7 +913,7 @@ function convoy.statusUpdate() -- every 10 seconds convoy.invokeDestroyedCallbacks(theConvoy) theZone = theConvoy.origin if theZone.deadOut then - theZone:pollFlag(theZone.deadOut, "inc") + theZone:pollFlag(theZone.deadOut, theZone.convoyMethod) -- "inc") end trigger.action.outTextForCoalition(theConvoy.coa, "Convoy " .. convName .. " enroute to " .. theConvoy.dest .. " was destroyed.", 30) trigger.action.outSoundForCoalition(theConvoy.coa, convoy.actionSound) diff --git a/modules/heloTroops.lua b/modules/heloTroops.lua index ebed7b3..a741ed1 100644 --- a/modules/heloTroops.lua +++ b/modules/heloTroops.lua @@ -1,5 +1,5 @@ cfxHeloTroops = {} -cfxHeloTroops.version = "3.1.5" +cfxHeloTroops.version = "4.0.0" cfxHeloTroops.verbose = false cfxHeloTroops.autoDrop = true cfxHeloTroops.autoPickup = false @@ -25,6 +25,9 @@ cfxHeloTroops.requestRange = 500 -- meters - loadSound and disembarkSound 3.1.4 - guarding destination access in save 3.1.5 - more guarding of destination access + 4.0.0 - added dropZones + - enforceDropZones + - coalition for drop zones --]]-- @@ -37,10 +40,24 @@ cfxHeloTroops.requiredLibs = { cfxHeloTroops.unitConfigs = {} -- all configs are stored by unit's name cfxHeloTroops.troopWeight = 100 -- kg average weight per trooper +cfxHeloTroops.dropZones = {} -- persistence support cfxHeloTroops.deployedTroops = {} +-- +-- drop zones +-- +function cfxHeloTroops.processDropZone(theZone) + theZone.droppedFlag = theZone:getStringFromZoneProperty("dropZone!", "cfxNone") + theZone.dropMethod = theZone:getStringFromZoneProperty("dropMethod", "inc") + theZone.dropCoa = theZone:getCoalitionFromZoneProperty("coalition", 0) + theZone.autoDespawn = theZone:getNumberFromZoneProperty("autoDespawn", -1) +end + +-- +-- comms +-- function cfxHeloTroops.resetConfig(conf) conf.autoDrop = cfxHeloTroops.autoDrop --if true, will drop troops on-board upon touchdown conf.autoPickup = cfxHeloTroops.autoPickup -- if true will load nearest troops upon touchdown @@ -576,16 +593,33 @@ function cfxHeloTroops.scoreWhenCapturing(theUnit) end end +function cfxHeloTroops.isInsideDropZone(theUnit) + local p = theUnit:getPoint() + for idx, theZone in pairs (cfxHeloTroops.dropZones) do + if theZone:isPointInsideZone(p) then return true end + end + return false +end + function cfxHeloTroops.doDeployTroops(args) local conf = args[1] local what = args[2] - -- deploy the troops I have on board in formation + local theUnit = conf.unit + local theGroup = theUnit:getGroup() + local gid = theGroup:getID() + local inside = cfxHeloTroops.isInsideDropZone(theUnit) + if (not inside) and cfxHeloTroops.enforceDropZones then + trigger.action.outTextForGroup(gid, "You are outside an disembark/drop zone.", 30) + return + end + + -- deploy the troops I have on board cfxHeloTroops.deployTroopsFromHelicopter(conf) -- interface with playerscore if we dropped -- inside an enemy-owned zone if cfxPlayerScore and cfxOwnedZones then - local theUnit = conf.unit + --local theUnit = conf.unit cfxHeloTroops.scoreWhenCapturing(theUnit) end @@ -630,7 +664,7 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf) end local chopperZone = cfxZones.createSimpleZone("choppa", p, 12) -- 12 m radius around choppa - local theCoalition = theUnit:getGroup():getCoalition() -- make it choppers COALITION + local theCoalition = theUnit:getGroup():getCoalition() -- make it chopper's COALITION local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( theCoalition, theName, -- group name, may be tracked @@ -677,8 +711,39 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf) end end end + + -- bang on all dropZones that we can find + for name, theZone in pairs(cfxHeloTroops.dropZones) do + -- can employ coalition test here as well, maybe later? + if theZone:isPointInsideZone(p) then + if theZone.dropCoa == 0 or theCoalition == theZone.dropCoa then + if cfxHeloTroops.verbose or theZone.verbose then + trigger.action.outText("+++Helo: will bang! on dropZone <" .. theZone.name .. "> output dropZone! <" .. theZone.droppedFlag .. "> with method <" .. theZone.dropMethod .. ">", 30) + end + theZone:pollFlag(theZone.droppedFlag, theZone.dropMethod) + end + if theZone.autoDespawn and theZone.autoDespawn > 0 then + args = {} + args.theZone = theZone + args.theGroup = theGroup + timer.scheduleFunction(cfxHeloTroops.autoDespawn, args, timer.getTime() + theZone.autoDespawn) + end + end + end end +function cfxHeloTroops.autoDespawn(args) + if not args then return end + local theZone = args.theZone + local theGroup = args.theGroup + if theZone.verbose then + trigger.action.outText("+++Helo: auto-despawning drop in drop zone <" .. theZone.name .. ">", 30) + end + if not theGroup then return end + if Group.isExist(theGroup) then + Group.destroy(theGroup) + end +end -- -- Loading Troops -- @@ -882,7 +947,7 @@ function cfxHeloTroops.readConfigZone() theZone = cfxZones.createSimpleZone("heloTroopsConfig") end - cfxHeloTroops.verbose = theZone:getBoolFromZoneProperty("verbose", false) + cfxHeloTroops.verbose = theZone.verbose if theZone:hasProperty("legalTroops") then local theTypesString = theZone:getStringFromZoneProperty("legalTroops", "") @@ -907,6 +972,7 @@ function cfxHeloTroops.readConfigZone() cfxHeloTroops.disembarkSound = theZone:getStringFromZoneProperty("disembarkSound", cfxHeloTroops.actionSound) cfxHeloTroops.requestRange = theZone:getNumberFromZoneProperty("requestRange", 500) + cfxHeloTroops.enforceDropZones = theZone:getBoolFromZoneProperty("enforceDropZones", false) -- add own troop carriers if theZone:hasProperty("troopCarriers") then local tc = theZone:getStringFromZoneProperty("troopCarriers", "UH-1D") @@ -999,6 +1065,13 @@ function cfxHeloTroops.start() -- read config zone cfxHeloTroops.readConfigZone() + -- read drop zones + local attrZones = cfxZones.getZonesWithAttributeNamed("dropZone!") + for k, aZone in pairs(attrZones) do + cfxHeloTroops.processDropZone(aZone) + cfxHeloTroops.dropZones[aZone.name] = aZone + end + -- start housekeeping cfxHeloTroops.houseKeeping() diff --git a/modules/playerScore.lua b/modules/playerScore.lua index 1d45727..cfe6209 100644 --- a/modules/playerScore.lua +++ b/modules/playerScore.lua @@ -1,5 +1,5 @@ cfxPlayerScore = {} -cfxPlayerScore.version = "3.3.1" +cfxPlayerScore.version = "4.0.0" cfxPlayerScore.name = "cfxPlayerScore" -- compatibility with flag bangers cfxPlayerScore.badSound = "Death BRASS.wav" cfxPlayerScore.scoreSound = "Quest Snare 3.wav" @@ -18,7 +18,9 @@ cfxPlayerScore.firstSave = true -- to force overwrite 3.3.0 - case INsensitivity for all typeScore objects 3.3.1 - fixes for DCS oddity in events after update - cleanup - + 4.0.0 - own event handling, disco from dcsCommon + - early landing detection (unitSpawnTime) + TODO: Kill event no longer invoked for map objetcs, attribute to faction now, reverse invocation direction with PlayerScore --]]-- @@ -51,7 +53,7 @@ cfxPlayerScore.train = 5 cfxPlayerScore.landing = 0 -- if > 0 it scores as feat cfxPlayerScore.unit2player = {} -- lookup and reverse look-up - +cfxPlayerScore.unitSpawnTime = {} -- lookup by unit name to prevent early landing function cfxPlayerScore.addSafeZone(theZone) theZone.scoreSafe = theZone:getCoalitionFromZoneProperty("scoreSafe", 0) table.insert(cfxPlayerScore.safeZones, theZone) @@ -570,7 +572,7 @@ function cfxPlayerScore.awardScoreTo(killSide, theScore, killerName) end -- --- EVENT HANDLING +-- EVENT PROCESSING / HANDLING -- function cfxPlayerScore.linkUnitWithPlayer(theUnit) -- create the entries for lookup and reverseLooup tables @@ -583,7 +585,7 @@ function cfxPlayerScore.unlinkUnit(theUnit) local uName = theUnit:getName() cfxPlayerScore.unit2player[uName] = nil end - +--[[-- function cfxPlayerScore.preProcessor(theEvent) -- return true if the event should be processed -- by us @@ -721,10 +723,13 @@ function cfxPlayerScore.preProcessor(theEvent) return false end +--]]-- +--[[-- function cfxPlayerScore.postProcessor(theEvent) -- don't do anything end +--]]-- function cfxPlayerScore.isStaticObject(theUnit) if not theUnit.getGroup then return true end @@ -1091,7 +1096,165 @@ function cfxPlayerScore.handlePlayerDeath(theEvent) end end -function cfxPlayerScore.handlePlayerEvent(theEvent) +-- +-- event detection +-- +function cfxPlayerScore.isScoreEvent(theEvent) + -- return true if the event results in a score event + if theEvent.initiator == nil then + return false + end + if cfxPlayerScore.verbose then + trigger.action.outText("Event preproc: " .. theEvent.id .. " (" .. dcsCommon.event2text(theEvent.id) .. ")", 30) + if theEvent.id == 8 or theEvent.id == 30 then -- dead or lost event + local who = theEvent.initiator + local name = "(nil ini)" + if who then + name = "(inval object)" + if who.getName then name = who:getName() end + end + trigger.action.outText("Dead/Lost subject: <" .. name .. ">", 30) + end + if theEvent.id == 2 then -- hit + local who = theEvent.initiator + local name = "(nil ini)" + if who then + name = "(inval initi)" + if who.getName then name = who:getName() end + if not name then -- WTF??? could be a weapon + name = "!nil getName!" + if who.getTypeName then name = who:getTypeName() end + if not name then + name = "WTFer" + end + end + end + + local hit = theEvent.object + local hname = "(nil ini)" + if hit then + hname = "(inval object)" + if hit.getName then hname = hit:getName() end + end + trigger.action.outText("o:<" .. name .. "> hit <" .. hname .. ">", 30) + end + end + + -- check if this was FORMERLY a player plane + local theUnit = theEvent.initiator + if not theUnit.getName then return end -- fix for DCS update bug + local uName = theUnit:getName() + if cfxPlayerScore.unit2player[uName] then + -- this requires special IMMEDIATE handling when event is + -- one of the below + if theEvent.id == 5 or -- crash + theEvent.id == 8 or -- dead + theEvent.id == 9 or -- pilot_dead + theEvent.id == 30 or -- unit loss + theEvent.id == 6 then -- eject + -- these can lead to a pilot demerit + -- event does NOT have a player + cfxPlayerScore.handlePlayerDeath(theEvent) + return false -- false = no score event (any more) + end + end + + -- from here on, initiator must be player + if not theUnit.getPlayerName or + not theUnit:getPlayerName() then + return false + end + if theEvent.id == 28 then -- kill, but only with target + local killer = theEvent.initiator + if not theEvent.target then + if cfxPlayerScore.verbose then + trigger.action.outText("+++scr kill nil TARGET", 30) + end + return false + end + -- if there are kill zones, we filter all kills that happen outside of kill zones + if #cfxPlayerScore.killZones > 0 then + local pLoc = theUnit:getPoint() + local tLoc = theEvent.target:getPoint() + local isIn, percent, dist, theZone = cfxZones.pointInOneOfZones(tLoc, cfxPlayerScore.killZones) + if not isIn then + if cfxPlayerScore.verbose then + trigger.action.outText("+++pScr: kill detected, but target <" .. theEvent.target:getName() .. "> was outside of any kill zones", 30) + end + return false + end + if theZone.duet and not cfxZones.pointInZone(pLoc, theZone) then + -- player must be in same zone but was not + if cfxPlayerScore.verbose then + trigger.action.outText("+++pScr: kill detected, but player <" .. theUnit:getPlayerName() .. "> was outside of kill zone <" .. theZone.name .. ">", 30) + end + return false + end + end + return true + end + + -- birth event for players initializes score if + -- not existed, and nils the queue + if theEvent.id == 15 then -- player birth + -- link player and unit + cfxPlayerScore.linkUnitWithPlayer(theUnit) + cfxPlayerScore.unitSpawnTime[uName] = timer.getTime() -- to detect 'early landing' +-- trigger.action.outText("Birth event", 30) + return true + end + + -- take off. overwrites timestamp for last landing + -- so a blipping t/o does nor count. Pre-proc only + if theEvent.id == 3 or theEvent.id == 54 then + local now = timer.getTime() + local playerName = theUnit:getPlayerName() + cfxPlayerScore.lastPlayerLanding[playerName] = now -- overwrite + return false + end + + -- landing can score. but only the first landing in x seconds + -- and has spawned more than 10 seconds before + -- landing in safe zone promotes any queued scores to + -- permanent if enabled, then nils queue + if theEvent.id == 4 or theEvent.id == 55 then +-- trigger.action.outText("LANDING event", 30) + -- player landed. filter multiple landed events + local now = timer.getTime() + local playerName = theUnit:getPlayerName() + -- if player spawns on ground, DCS now can post a + -- "landing" event. filter + if cfxPlayerScore.unitSpawnTime[uName] and + now - cfxPlayerScore.unitSpawnTime[uName] < 10 + then + cfxPlayerScore.lastPlayerLanding[playerName] = now -- just for the sake of it +-- trigger.action.outText("(DCS early landing bug ignored)", 30) + return false + end +-- trigger.action.outText("Time since spawn: " .. now - cfxPlayerScore.unitSpawnTime[uName], 30) + local lastLanding = cfxPlayerScore.lastPlayerLanding[playerName] + cfxPlayerScore.lastPlayerLanding[playerName] = now -- overwrite + if lastLanding and lastLanding + cfxPlayerScore.delayBetweenLandings > now then + if cfxPlayerScore.verbose then + trigger.action.outText("+++pScr: Player <" .. playerName .. "> touch-down ignored: too soon after last.", 30) + trigger.action.outText("now is <" .. now .. ">, between is <" .. cfxPlayerScore.delayBetweenLandings .. ">, last + between is <" .. lastLanding + cfxPlayerScore.delayBetweenLandings .. ">", 30) + end + -- filter this event, too soon + return false + end + return true + end + + return false +end + +function cfxPlayerScore:onEvent(theEvent) + if cfxPlayerScore.isScoreEvent(theEvent) then + cfxPlayerScore.handleScoreEvent(theEvent) + end +end + +function cfxPlayerScore.handleScoreEvent(theEvent) if theEvent.id == 28 then -- kill from player detected. cfxPlayerScore.killDetected(theEvent) @@ -1134,7 +1297,9 @@ function cfxPlayerScore.handlePlayerEvent(theEvent) end end end - +-- +-- Config handling +-- function cfxPlayerScore.readConfigZone(theZone) cfxPlayerScore.verbose = theZone.verbose -- default scores @@ -1450,9 +1615,10 @@ function cfxPlayerScore.start() end -- subscribe to events and use dcsCommon's handler structure - dcsCommon.addEventHandler(cfxPlayerScore.handlePlayerEvent, - cfxPlayerScore.preProcessor, - cfxPlayerScore.postProcessor) + -- dcsCommon.addEventHandler(cfxPlayerScore.handlePlayerEvent, +-- cfxPlayerScore.preProcessor, +-- cfxPlayerScore.postProcessor) + world.addEventHandler(cfxPlayerScore) -- now load all save data and populate map with troops that -- we deployed last save. if persistence then diff --git a/modules/radioMenus.lua b/modules/radioMenus.lua index 457495a..768d45b 100644 --- a/modules/radioMenus.lua +++ b/modules/radioMenus.lua @@ -21,6 +21,7 @@ radioMenu.lateGroups = {} -- dict by ID - added dynamic player support 4.0.1 - MX no longer optional, so ask for it 4.0.2 - ackSnd now also broadcasts to all if no group + 4.1.0 - outX --> valX verification. putting back in optional outX --]]-- function radioMenu.addRadioMenu(theZone) @@ -437,6 +438,11 @@ function radioMenu.createRadioMenuWithZone(theZone) if theZone:hasProperty("radioMethod") then theZone.radioMethod = theZone:getStringFromZoneProperty( "radioMethod", "inc") end + -- note: outX currently overridden with valX + theZone.outMethA = theZone:getStringFromZoneProperty("outA", theZone.radioMethod) + theZone.outMethB = theZone:getStringFromZoneProperty("outB", theZone.radioMethod) + theZone.outMethC = theZone:getStringFromZoneProperty("outC", theZone.radioMethod) + theZone.outMethD = theZone:getStringFromZoneProperty("outD", theZone.radioMethod) theZone.radioTriggerMethod = theZone:getStringFromZoneProperty("radioTriggerMethod", "change") @@ -731,6 +737,7 @@ function radioMenu.doMenuX(args) local outVal = theZone.outValA local ack = theZone.ackA local ackSnd = theZone.ackASnd + local meth = theZone.outMethA -- currently not used -- decode A..X if theItemIndex == "B"then @@ -740,6 +747,7 @@ function radioMenu.doMenuX(args) outVal = theZone.outValB ack = theZone.ackB ackSnd = theZone.ackBSnd + meth = theZone.outMethB elseif theItemIndex == "C" then cd = radioMenu.cdByGID(theZone.mcdC, theZone, theGroup) -- theZone.mcdC busy = theZone.busyC @@ -747,6 +755,7 @@ function radioMenu.doMenuX(args) outVal = theZone.outValC ack = theZone.ackC ackSnd = theZone.ackCSnd + meth = theZone.outMethC elseif theItemIndex == "D" then cd = radioMenu.cdByGID(theZone.mcdD, theZone, theGroup) -- theZone.mcdD busy = theZone.busyD @@ -754,6 +763,7 @@ function radioMenu.doMenuX(args) outVal = theZone.outValD ack = theZone.ackD ackSnd = theZone.ackDSnd + meth = theZone.outMethD end -- see if we are on cooldown @@ -793,6 +803,7 @@ function radioMenu.doMenuX(args) -- poll flag, override with outVal if set if outVal then --outVal = "#"..outVal -- we force immediate mode + -- now replaced by 'valX' attribute theZone:pollFlag(theFlag, outVal) if theZone.verbose or radioMenu.verbose then trigger.action.outText("+++menu: overriding index " .. theItemIndex .. " output method <" .. theZone.radioMethod .. "> with immediate value <" .. outVal .. ">", 30) diff --git a/modules/theDebugger.lua b/modules/theDebugger.lua index 892f8f8..9acf2d8 100644 --- a/modules/theDebugger.lua +++ b/modules/theDebugger.lua @@ -1,5 +1,5 @@ debugger = {} -debugger.version = "3.0.0" +debugger.version = "3.1.0" debugDemon = {} debugDemon.version = "2.1.0" @@ -45,7 +45,8 @@ debugger.log = "" - x *f - x *z - x ? - + 3.1.0 - new DCS world events 57-61 + - x ! xref hanging/unconnected --]]-- @@ -122,7 +123,11 @@ debugDemon.eventList = { ["54"] = "S_EVENT_MISSION_WINNER = 54", ["55"] = "S_EVENT_POSTPONED_TAKEOFF = 55", ["56"] = "S_EVENT_POSTPONED_LAND = 56", - ["57"] = "S_EVENT_MAX = 57", + ["57"] = "S_EVENT_SIMULATION_FREEZE = 57", + ["58"] = "S_EVENT_SIMULATION_UNFREEZE = 58", + ["59"] = "S_EVENT_HUMAN_AIRCRAFT_REPAIR_START = 59", + ["60"] = "S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH = 60", + ["61"] = "S_EVENT_MAX = 61", } debugger.spawnTypes = { @@ -150,7 +155,7 @@ debugger.spawnTypes = { -- XREF MODULE -- xref = {} -xref.version = "1.0.0" +xref.version = "1.1.0" xref.dmlObjects = {} -- dict by zone name:upper() -- has inputs: dict of string for each '?' input, contains input flag name -- has output: dict of array for each output '!', contains output flag names as array @@ -158,7 +163,7 @@ xref.flags = {} -- dict by flag name -- has froms: dict of zone of attributes that can write to this flags -- has tos: dict of zones of attributes that read from flag -function xref.getDmlObject(name) +function xref.getDmlObject(name) -- lazy alloc name = name:upper() local theObject = xref.dmlObjects[name] if not theObject then @@ -170,7 +175,7 @@ function xref.getDmlObject(name) return theObject end -function xref.getFlag(name, theZone) +function xref.getFlag(name, theZone) -- lazy alloc if theZone and dcsCommon.stringStartsWith(name, "*") then -- local name conversion name = theZone.name .. name @@ -178,8 +183,8 @@ function xref.getFlag(name, theZone) local theFlag = xref.flags[name] if not theFlag then theFlag = {} - theFlag.froms = {} -- dict by zone name/output that write to this flag - theFlag.tos = {} -- dict by zone name / inputs that reads from this flag + theFlag.froms = {} -- dict by zone name: OUTPUTS that write to this flag + theFlag.tos = {} -- dict by zone name: INPUTS that read from this flag xref.flags[name] = theFlag end return theFlag @@ -236,20 +241,21 @@ function xref.scanMissionZones() -- iterate all properties local attributes = theZone:getAllZoneProperties() for name, value in pairs(attributes) do + local n2 = dcsCommon.trim(name) -- find inputs - if dcsCommon.stringEndsWith(name, "?") then - xref.addInput(theZone, name, value) + if dcsCommon.stringEndsWith(n2, "?") then + xref.addInput(theZone, n2, value) end -- find outputs - if dcsCommon.stringEndsWith(name, "!") then - xref.addOutput(theZone, name, value) + if dcsCommon.stringEndsWith(n2, "!") then + xref.addOutput(theZone, n2, value) end -- other stuff, e.g. "#" -- find outputs - if dcsCommon.stringEndsWith(name, "#") then - xref.addOutput(theZone, name, value) + if dcsCommon.stringEndsWith(n2, "#") then + xref.addOutput(theZone, n2, value) end end end @@ -289,8 +295,6 @@ function xref.xrefFlag(name) end end end - --- trigger.action.outText(msg, 30) return msg end @@ -323,9 +327,7 @@ function xref.xrefZone(name, theObject) end end --- trigger.action.outText(msg, 30) - return msg - + return msg end function xref.xrefName(name) @@ -362,41 +364,93 @@ function xref.allZones() end function xref.xall() - msg = "" + local msg = "" -- now dump all flags for flagName, data in pairs (xref.flags) do - -- msg = msg .. xref.xrefFlag(flagName) msg = msg .. xref.xrefName(flagName) end -- dump all zones for zoneName, data in pairs(xref.dmlObjects) do --- msg = msg .. xref.xrefZone(zoneName, data) msg = msg .. xref.xrefName(zoneName) end return msg --- trigger.action.outText(msg, 30) end +function xref.unconnected() + local msg = "xref: unconnected flags/commands:\n" + local badFroms = {} + local badTos = {} + for name, theFlag in pairs(xref.flags) do + -- look for single-direction flags, i.e those that have + -- no froms or no tos. + if dcsCommon.getSizeOfTable(theFlag.froms) < 1 then + -- this flag has output referencing it, so the input hangs + badFroms[name] = theFlag + end + if dcsCommon.getSizeOfTable(theFlag.tos) < 1 then + -- this flag has output referencing it, so the input hangs + badTos[name] = theFlag + end + end + local hasUnconnected = false + if dcsCommon.getSizeOfTable(badFroms) > 0 then + msg = msg .. " used at [zone]:INPUT that listens for/to nothing:\n" + for name, theFlag in pairs(badFroms) do + msg = msg .. " <" .. name .. ">: " + local froms = theFlag.tos -- crossing from and to here! + for zName, outList in pairs(froms) do + msg = msg .. "[" .. zName .. "]:" + local c = 0 + for idx, outName in pairs(outList) do + if c > 0 then msg = msg .. ", " end + c = 1 + msg = msg .. outName + end + msg = msg .. "\n" + end + end + msg = msg .. " -- END OF LIST --\n\n" + hasUnconnected = true + end + + if dcsCommon.getSizeOfTable(badTos) > 0 then + msg = msg .. " used at [zone]:OUTPUT that sends to nobody:\n" + for name, theFlag in pairs(badTos) do + msg = msg .. " <" .. name .. ">: " + local tos = theFlag.froms -- crossing! + for zName, inList in pairs(tos) do + msg = msg .. "[" .. zName .. "]:" + local c = 0 + for idx, inName in pairs(inList) do + if c > 0 then msg = msg .. ", " end + c = 1 + msg = msg .. inName + end + end + msg = msg .. "\n" + end + hasUnconnected = true + msg = msg .. " -- END OF LIST --\n\n" + end + + if not hasUnconnected then + msg = msg .. "\n -- NONE --\n" + end + + return msg +end + function xref.start() xref.scanMissionZones() local flagNum = dcsCommon.getSizeOfTable(xref.flags) local dmlObNum = dcsCommon.getSizeOfTable(xref.dmlObjects) - trigger.action.outText("XRef v" .. xref.version .. " full DML object scan on mission complete:\n<" .. flagNum .. "> flags are referenced in <" .. dmlObNum .. "> DML zones", 30) - --- trigger.action.outText(xref.xall(), 30) + trigger.action.outText("XRef v" .. xref.version .. " full DML object scan on mission complete:\n<" .. flagNum .. "> flags are referenced in <" .. dmlObNum .. "> DML zones", 30) end -- run the xref xref.start() ---[[-- - to do - scan messenger and wildcards for flag access - ---]]-- - - -- -- DEBUGGER MAIn -- @@ -436,7 +490,6 @@ function debugger.saveLog(name) end end - -- -- tracking flags -- @@ -764,7 +817,6 @@ function debugger.update() end end - -- -- Config & Start -- @@ -950,7 +1002,6 @@ function debugDemon:onEvent(theEvent) end if theEvent.id == world.event.S_EVENT_MARK_CHANGE then --- trigger.action.outText("debugger: Mark Change event received", 30) -- 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 @@ -958,42 +1009,26 @@ function debugDemon:onEvent(theEvent) -- else an error is displayed and the mark remains. if debugDemon.hasMark(theEvent.text) then -- strip the mark - local cCommand = dcsCommon.clone(theEvent.text, true) + local cCommand = dcsCommon.clone(theEvent.text) local commandString = cCommand:sub(1+debugDemon.markOfDemon:len()) -- break remainder apart into ... local commands = dcsCommon.splitString(commandString, debugDemon.splitDelimiter) -- this is a command. process it and then remove it if it was executed successfully - local cTheEvent = dcsCommon.clone(theEvent, true) -- strip meta tables + local cTheEvent = dcsCommon.clone(theEvent) -- strip meta tables local args = {commands, cTheEvent} -- defer execution for 0.1s to get out of trx bracked timer.scheduleFunction(debugDemon.deferredDebug, args, timer.getTime() + 0.1) debugDemon.activeIdx = cTheEvent.idx - --[[-- - local success = debugDemon.executeCommand(commands, cTheEvent) -- execute on a clone, not original - - -- remove this mark after successful execution - if success then - trigger.action.removeMark(theEvent.idx) - else - -- we could play some error sound - end - --]]-- end end if theEvent.id == world.event.S_EVENT_MARK_REMOVED then --- trigger.action.outText("Mark Remove received, removing idx <" .. theEvent.idx .. ">.", 30) debugDemon.activeIdx = nil end end function debugDemon.deferredDebug(args) --- trigger.action.outText("enter deferred debug command", 30) --- if not debugDemon.activeIdx then --- trigger.action.outText("Debugger: window was closed, debug command ignored.", 30) --- return --- end local commands = args[1] local cTheEvent = args[2] local success = debugDemon.executeCommand(commands, cTheEvent) -- execute on a clone, not original @@ -1884,8 +1919,11 @@ function debugDemon.processXrefCommand(args, event) if not larg or larg == "" then larg = "?" end larg = larg:lower() if larg == "?" then - debugger.outText("*** xRef: ? = help (this), * = xref all, *f = list all DML flags, *z = list all DML zones, xref flag or zone", 30) + debugger.outText("*** xRef: ? = help (this), ! = xref all 'hanging' or unconnected flags/commands, * = xref all, *f = list all DML flags, *z = list all DML zones, xref flag or zone", 30) return true -- leave up + elseif larg == "!" then + debugger.outText(xref.unconnected(), 30) + return true elseif larg == "*" then debugger.outText(xref.xall(), 30) return true diff --git a/tutorial & demo missions/demo - Inferno at Sea.miz b/tutorial & demo missions/demo - Inferno at Sea.miz new file mode 100644 index 0000000..4f583aa Binary files /dev/null and b/tutorial & demo missions/demo - Inferno at Sea.miz differ diff --git a/tutorial & demo missions/demo - The Yeller Model.miz b/tutorial & demo missions/demo - The Yeller Model.miz new file mode 100644 index 0000000..b4f04db Binary files /dev/null and b/tutorial & demo missions/demo - The Yeller Model.miz differ diff --git a/tutorial & demo missions/demo - helo cargo.miz b/tutorial & demo missions/demo - helo cargo.miz index 243ea67..4c2f11c 100644 Binary files a/tutorial & demo missions/demo - helo cargo.miz and b/tutorial & demo missions/demo - helo cargo.miz differ