diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 389b4a7..aa18be0 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 08bcf78..5e061d2 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 f24788b..9b9696f 100644 --- a/modules/RNDFlags.lua +++ b/modules/RNDFlags.lua @@ -12,27 +12,7 @@ rndFlags.requiredLibs = { Copyright 2022 by Christian Franz and cf/x Version History - 1.0.0 - Initial Version - 1.1.0 - DML flag conversion: - flagArrayFromString: strings OK, trim - remove pollFlag - pollFlag from cfxZones, include zone - randomBetween for pollSize - pollFlag to bang done with inc - getFlagValue in update - some code clean-up - rndMethod synonym - 1.2.0 - Watchflag integration - 1.3.0 - DML simplification: RND! - zone-local verbosity - 1.3.1 - 'done+1' --> 'done!', using rndMethod instead of 'inc' - - added zonal verbosity - - added 'rndDone!' flag - - rndMethod defaults to "inc" - 1.3.2 - moved flagArrayFromString to dcsCommon - - minor clean-up - 1.4.0 - persistence - 1.4.1 - a little less verbosity + 2.0.0 - dmlZones, OOP --]] diff --git a/modules/SSBSingleUse.lua b/modules/SSBSingleUse.lua index aae2ffd..bfa4095 100644 --- a/modules/SSBSingleUse.lua +++ b/modules/SSBSingleUse.lua @@ -158,3 +158,13 @@ if not cfxSSBSingleUse.start()then trigger.action.outText("SSB Single Use failed to start up.", 30) cfxSSBSingleUse = nil end + + +--[[-- + limited use rebuild + have planes inside a zone to regulate: + numUp attribute for number of lives. defaults to one + reinforce? to make availabe again, resets numLeft to numUp - warning: check with other inhibitors + onStart = no will inhibit at start + exempt - limits to not apply to planes in this zone. same as -1 for numUp +--]]-- \ No newline at end of file diff --git a/modules/TDZ.lua b/modules/TDZ.lua index 380d60b..cc50639 100644 --- a/modules/TDZ.lua +++ b/modules/TDZ.lua @@ -1,5 +1,5 @@ tdz = {} -tdz.version = "1.0.2" +tdz.version = "1.0.3" tdz.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -17,7 +17,8 @@ VERSION HISTORY helo attribute 1.0.2 - manual placement option filters FARPs - + 1.0.3 - "manual" now defaults to false + --]]-- tdz.allTdz = {} @@ -89,7 +90,7 @@ function tdz.createTDZ(theZone) local nearestRwy = nil -- see if this is a manually placed runway - if theZone:getBoolFromZoneProperty("manual", true) then + if theZone:getBoolFromZoneProperty("manual", false) then -- construct runway from trigger zone attributes if theZone.verbose or tdz.verbose then trigger.action.outText("+++TDZ: runway for <" .. theZone.name .. "> is manually placed", 30) diff --git a/modules/airfield.lua b/modules/airfield.lua index e3f10e0..44c35f4 100644 --- a/modules/airfield.lua +++ b/modules/airfield.lua @@ -1,34 +1,26 @@ airfield = {} -airfield.version = "2.0.0" +airfield.version = "2.1.1" airfield.requiredLibs = { "dcsCommon", "cfxZones", } -airfield.myAirfields = {} -- indexed by name +airfield.myAirfields = {} -- indexed by af name, zone that links to it airfield.gracePeriod = 3 -airfield.allAirfields = {} -- inexed by name +airfield.allAirfields = {} -- inexed by af name, db entries: base, cat --[[-- This module generates signals when the nearest airfield changes hands, can force the coalition of an airfield, and always provides the current owner as a value - Version History - 1.0.0 - initial release - 1.1.0 - added 'fixed' attribute - - added 'farps' attribute to individual zones - - allow zone.local farps designation - - always checks farp cap events - - added verbosity - 1.1.1 - GC grace period correction of ownership - 1.1.2 - 'show' attribute - line color attributes per zone - line color defaults in config + Version History^ 2.0.0 - show all airfields option - fully reworked show options - unmanaged airfields are automatically updated - full color support -- support for FARPS as well + 2.1.0 - added support for makeNeutral? + 2.1.1 - bug fixing for DCS 2.9x airfield retrofit --]]-- @@ -39,7 +31,9 @@ function airfield.collectAll() local dropped = 0 for idx, aBase in pairs(allBases) do local entry = {} - local cat = Airbase.getCategory(aBase) -- DCS 2.9 hardened + --local cat = Airbase.getCategory(aBase) -- DCS 2.9 hardened + -- ho! dcs 2.9.x retrofit screwed with Airfield.getCategory. + local cat = aBase:getDesc().category -- cats: 0 = airfield, 1 = farp, 2 = ship if (cat == 0) or (cat == 1) then local name = aBase:getName() @@ -50,6 +44,7 @@ function airfield.collectAll() count = count + 1 else dropped = dropped + 1 +-- trigger.action.outText("***dropped airbase <" .. aBase:getName() .. ">, cat = <" .. cat .. ">", 30) end end if airfield.verbose then @@ -98,6 +93,11 @@ function airfield.createAirFieldFromZone(theZone) theZone.lastMakeBlue = trigger.misc.getUserFlag(theZone.makeBlue) end + if theZone:hasProperty("makeNeutral?") then + theZone.makeNeutral = theZone:getStringFromZoneProperty("makeNeutral?", "") + theZone.lastMakeNeutral = trigger.misc.getUserFlag(theZone.makeNeutral) + end + if theZone:hasProperty("autoCap?") then theZone.autoCap = theZone:getStringFromZoneProperty("autoCap?", "") theZone.lastAutoCap = trigger.misc.getUserFlag(theZone.autoCap) @@ -146,8 +146,11 @@ function airfield.createAirFieldFromZone(theZone) -- now mark this zone as handled local entry = airfield.allAirfields[theZone.afName] - entry.linkedTo = theZone -- only remember last, but that's enough. - + if not entry then + trigger.action.outText("+++airF: WARNING - unlinked airfield <" .. theZone.afName .. "> for zone <" .. theZone.name .. ">", 30) + else + entry.linkedTo = theZone -- only remember last, but that's enough. + end end function airfield.markAirfieldOnMap(theAirfield, lineColor, fillColor) @@ -246,6 +249,10 @@ function airfield.airfieldCaptured(theBase) airfield.untendedCapture(bName, theBase) return end -- not attached to a zone + if theZone.verbose or airfield.verbose then + trigger.action.outText("+++airF: capturing <" .. bName .. "> for zone <" .. theZone.name .. ">", 30) + end + local newCoa = theBase:getCoalition() theZone.owner = newCoa @@ -323,6 +330,21 @@ function airfield.update() airfield.airfieldCaptured(theAirfield) end end + + if theZone.makeNeutral and theZone:testZoneFlag(theZone.makeNeutral, theZone.triggerMethod, "lastMakeNeutral") then + if theZone.verbose or airfield.verbose then + trigger.action.outText("+++airF: 'makeNeutral' triggered for airfield <" .. afName .. "> in zone <" .. theZone:getName() .. ">", 30) + end + if theAirfield:autoCaptureIsOn() then + -- turn off autoCap + airfield.assumeControl(theZone) + end + theAirfield:setCoalition(0) -- make it blue + if theZone.owner ~= 0 then -- only send cap event when capped + airfield.airfieldCaptured(theAirfield) -- 0 cap will not cause any signals, but we do this anyway + end + end + if theZone.autoCap and theZone:testZoneFlag(theZone.autoCap, theZone.triggerMethod, "lastAutoCap") then if theAirfield:autoCaptureIsOn() then diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index f217452..c2a1ee6 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "4.1.1" +cfxZones.version = "4.1.2" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -43,6 +43,7 @@ cfxZones.version = "4.1.1" - 4.0.10 - getBoolFromZoneProperty also supports "on" (=true) and "off" (=false) - 4.1.0 - getBoolFromZoneProperty 'on/off' support for dml variant as well - 4.1.1 - evalRemainder() updates +- 4.1.2 - hash property missing warning --]]-- @@ -2299,6 +2300,14 @@ function dmlZone:hasProperty(theProperty) return false end + if string.sub(theProperty, -1) == "#" then + local lessOp = theProperty:sub(1,-2) + if self:getZoneProperty(lessOp) ~= nil then + trigger.action.outText("*** NOTE: " .. self.name .. "'s property <" .. lessOp .. "> may be missing a hash mark ('#') at end", 30) + end + return false + end + return false end return true diff --git a/modules/civAir.lua b/modules/civAir.lua index 7de8f10..d4b0a63 100644 --- a/modules/civAir.lua +++ b/modules/civAir.lua @@ -1,37 +1,6 @@ civAir = {} -civAir.version = "3.0.1" +civAir.version = "3.0.2" --[[-- - 1.0.0 initial version - 1.1.0 exclude list for airfields - 1.1.1 bug fixes with remove flight - and betweenHubs - check if slot is really free before spawning - add overhead waypoint - 1.1.2 inAir start possible - 1.2.0 civAir can use own config file - 1.2.1 slight update to config file (moved active/idle) - 1.3.0 add ability to use zones to add closest airfield to - trafficCenters or excludeAirfields - 1.4.0 ability to load config from zone to override - all configs it finds - module check - removed obsolete civAirConfig module - 1.5.0 process zones as in all other modules - verbose is part of config zone - reading type array from config corrected - massive simplifications: always between zoned airfieds - exclude list and include list - 1.5.1 added depart only and arrive only options for airfields - 1.5.2 fixed bugs inb verbosity - 2.0.0 dmlZones - inbound zones - outbound zones - on start location is randomizes 30-70% of the way - guarded 'no longer exist' warning for verbosity - changed unit naming from -civA to -GA - strenghtened guard on testing against free slots for other units - flights are now of random neutral countries - maxFlights synonym for maxTraffic 3.0.0 liveries support default liveries for Yak-50 (main test case) default liveries for C-130, c-17A, IL-76MD, An-30M, An-26B @@ -45,6 +14,7 @@ civAir.version = "3.0.1" 3.0.1 protest option, on by default protest action spawning now works correctly for groupType + 3.0.2 clean-up --]]-- diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index 5daabb3..25b7376 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -259,6 +259,14 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" trigger.action.outText("+++clnZ: masterOwner for <" .. theZone.name .. "> set successfully to to itself, currently owned by faction <" .. theZone.owner .. ">", 30) end end + if theZone.verbose or cloneZones.verbose then + trigger.action.outText("+++clnZ: ownership of <" .. theZone.name .. "> tied to zone <" .. theZone.masterOwner .. ">", 30) + end + -- check that the zone exists in DCS + local theMaster = cfxZones.getZoneByName(theZone.masterOwner) + if not theMaster then + trigger.action.outText("clnZ: WARNING: cloner's <" .. theZone.name .. "> master owner named <" .. theZone.masterOwner .. "> does not exist!", 30) + end end theZone.turn = theZone:getNumberFromZoneProperty("turn", 0) diff --git a/modules/csarManager2.lua b/modules/csarManager2.lua index ba5d2e0..f65baa7 100644 --- a/modules/csarManager2.lua +++ b/modules/csarManager2.lua @@ -1,70 +1,9 @@ csarManager = {} -csarManager.version = "2.3.2" -csarManager.verbose = false +csarManager.version = "3.0.0" csarManager.ups = 1 --[[-- VERSION HISTORY - - 1.0.0 initial version - - 1.0.1 - smoke optional - - airframeCrashed method for airframe manager - - removed '(downed )' when re-picked up - - fixed oclock - - 1.0.2 - hover retrieval - - 1.0.3 - corrected a bug in oclock during hovering - - 1.0.4 - now correctly allocates pilot to coalition via dcscommon.coalition2county - - 1.1.0 - pilot adds weight to unit - - module check - - 2.0.0 - weight managed via cargoSuper - - 2.0.1 - getCSARBaseforZone() - - check if zone landed in has owner attribute - to provide compatibility with owned zones, - FARPZones etc that keep zone.owner up to date - - 2.0.2 - use parametric csarManager.hoverAlt - - use hoverDuration - - 2.0.3 - corrected bug in hoverDuration - - 2.0.4 - guard in createCSARMission for cfxCommander - - 2.1.0 - startCSAR? - - deferrable missions - - verbose - - ups - - useSmoke - - smokeColor - - reworked smoking the loc - - config zone - - csarRedDelivered - - csarBlueDelivered - - finally fixed smoke performance bug - - csarManager.vectoring optional - - 2.1.1 - zone-local verbosity - - 2.1.2 - 'downed' machinations (paranthese)S - - verbosity - - 2.1.3 - theMassObject now local - - winch pickup now also adds weight so they can be returned - - made some improvements to performance by making vars local - - 2.2.0 - interface for autoCSAR - createDownedPilot() - added existingUnit option - createCSARMissionData() - added existinUnit option - - when no config zone, runs through empty zone - - actionSound - - integration with playerScore - - score global and per-mission - - isCSARTarget API - - 2.2.1 - added troopCarriers attribute to config - - passes own troop carriers to dcsCommon.isTroopCarrier() - - 2.2.2 - enable CSAR missions in water - - csar name defaults to zone name - - better randomization of pilot's point in csar mission, - supports quad zone - - 2.2.3 - better support for red/blue - - allow neutral pick-up - - directions to closest safe zone - - CSARBASE attribute now carries coalition - - deprecated coalition attribute - - 2.2.4 - CSAR attribute value defaults name - - start? attribute for CSAR as startCSAR? synonym - - 2.2.5 - manual freq for CSAR was off by a factor of 10 - Corrected - - 2.2.6 - useFlare, now also launches a flare in addition to smoke - - zone testing uses getPoint for zones, supports moving csar zones + - 2.3.0 - dmlZones - onRoad attribute for CSAR mission Zones - rndLoc support @@ -74,32 +13,27 @@ csarManager.ups = 1 - offset zone on randomized soldier - smokeDist - 2.3.2 - DCS 2.9 getCategory() fix + - 3.0.0 - moved mission creation out of update loop into own + - removed cfxPlayer dependency + - new event manager + - no longer single-proccing pilots + - can also handle aircraft - isTroopCarrier INTEGRATES AUTOMATICALLY WITH playerScore IF INSTALLED + INTEGRATES WITH LIMITE AIRFRAMES IF INSTALLED --]]-- -- modules that need to be loaded BEFORE I run csarManager.requiredLibs = { "dcsCommon", -- common is of course needed for everything "cfxZones", -- zones management for CSAR and CSAR Mission zones - "cfxPlayer", -- player monitoring and group monitoring "nameStats", -- generic data module for weight "cargoSuper", --- "cfxCommander", -- needed only if you want to hand-create CSAR missions } - --- --- OPTIONS --- -csarManager.useSmoke = true -csarManager.smokeColor = 4 -- when using smoke - - -- unitConfigs contain the config data for any helicopter -- currently in the game. The Array is indexed by unit name csarManager.unitConfigs = {} -csarManager.myEvents = {3, 4, 5} -- 3 = take off, 4 = land, 5 = crash -- -- CASR MISSION @@ -156,16 +90,11 @@ function csarManager.createDownedPilot(theMission, existingUnit) aLocation.x, aLocation.z, -aHeading + 1.5) -- + 1.5 to turn inwards - - -- WARNING: - -- coalition.addGroup takes the COUNTRY of the group, and derives the - -- coalition from that. So if mission.sie is 0, we use UN, if it is 1 (red) it - -- is joint red, if 2 it is joint blue - local theSideCJTF = dcsCommon.coalition2county(theMission.side) -- get the correct county CJTF + + local theSideCJTF = dcsCommon.getACountryForCoalition(theMission.side) theMission.group = coalition.addGroup(theSideCJTF, Group.Category.GROUND, theBoyGroup) - if theBoyGroup then else @@ -176,10 +105,10 @@ function csarManager.createDownedPilot(theMission, existingUnit) end -- we now use commands to send radio transmissions - local ADF = 20 + math.random(90) + local ADF = 20 + math.random(150) -- create a random number between 20 and 110 --> 200'000 .. 1'700'000 KHz = 200KHz .. 1'700 KHz if theMission.freq then ADF = theMission.freq else theMission.freq = ADF end local theCommands = cfxCommander.createCommandDataTableFor(theMission.group) - local cmd = cfxCommander.createSetFrequencyCommand(ADF) -- freq in 10000 Hz + local cmd = cfxCommander.createSetFrequencyCommand(ADF) -- freq in 10'000 Hz cfxCommander.addCommand(theCommands, cmd) cmd = cfxCommander.createTransmissionCommand(csarManager.beaconSound) cfxCommander.addCommand(theCommands, cmd) @@ -259,7 +188,7 @@ end -- UNIT CONFIG -- function csarManager.resetConfig(conf) - -- reset only ovberwrites mission-relevant data + -- reset only overwrites mission-relevant data conf.troopsOnBoard = {} -- number of rescued missions local myName = conf.name cargoSuper.removeAllMassForCargo(myName, "Evacuees") -- will allocate new empty table @@ -309,47 +238,17 @@ end -- -- E V E N T H A N D L I N G -- -function csarManager.isInteresting(eventID) - -- return true if we are interested in this event, false else - for key, evType in pairs(csarManager.myEvents) do - if evType == eventID then return true end - end - return false -end - -function csarManager.preProcessor(event) +function csarManager:onEvent(event) -- make sure it has an initiator - if not event.initiator then return false end -- no initiator + if not event.initiator then return end local theUnit = event.initiator - if not theUnit.getDesc then return fase end -- not a unit - local cat = theUnit:getDesc().category --theUnit:getCategory() - if cat ~= 1 then -- Unit.Category.HELICOPTER - return false - end - - --trigger.action.outText("+++csar: event " .. event.id .. " for cat = " .. cat .. " (helicopter?) unit " .. theUnit:getName(), 30) - - if not cfxPlayer.isPlayerUnit(theUnit) then - --trigger.action.outText("+++csar: rejected event: " .. theUnit:getName() .. " not a player helo", 30) - return false - end -- not a player unit - return csarManager.isInteresting(event.id) -end + + if not dcsCommon.isPlayerUnit(theUnit) then return end -- not a player unit -function csarManager.postProcessor(event) - -- don't do anything for now -end + -- only proceed if troop carrier (no more helo checks, all troop carriers, so osprey and harrier can be used if so desired) + if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end -function csarManager.somethingHappened(event) - -- when this is invoked, the preprocessor guarantees that - -- it's an interesting event - -- unit is valid and player - -- airframe category is helicopter - local theUnit = event.initiator local ID = event.id - - local myType = theUnit:getTypeName() - if ID == 4 then -- landed csarManager.heloLanded(theUnit) end @@ -360,17 +259,18 @@ function csarManager.somethingHappened(event) if ID == 5 then -- crash csarManager.heloCrashed(theUnit) + end + + if ID == 15 then -- player helicopter birth + -- we need to set up comms for this unit + csarManager.setCommsMenu(theUnit) end - csarManager.setCommsMenu(theUnit) end --- -- -- CSAR LANDED -- --- - function csarManager.successMission(who, where, theMission) -- who is -- where is @@ -397,7 +297,6 @@ function csarManager.successMission(who, where, theMission) -- now call callback for coalition side -- callback has format callback(coalition, success true/false, numberSaved, descriptionText) - csarManager.invokeCallbacks(theMission.side, true, 1, "success") trigger.action.outSoundForCoalition(theMission.side, csarManager.actionSound) -- "Quest Snare 3.wav") @@ -491,13 +390,11 @@ function csarManager.heloLanded(theUnit) conf.troopsOnBoard = {} -- empty out troops on board -- we do *not* return so we can pick up troops on -- a CSARBASE if they were dropped there - else if csarManager.verbose or base.zone.verbose then trigger.action.outText("+++csar: touchdown of <" .. myName .. "> occured outside of csar zone <" .. base.zone.name .. ">", 30) end end - else -- not on my side if csarManager.verbose or base.zone.verbose then trigger.action.outText("+++csar: base <" .. base.zone.name .. "> is on side <" .. currentBaseSide .. ">, which is not on my side <" .. mySide .. ">.", 30) @@ -930,46 +827,6 @@ function csarManager.directions(args) trigger.action.outTextForGroup(conf.id, report, 30) trigger.action.outSoundForGroup(conf.id, csarManager.actionSound) end --- --- Player event callbacks --- -function csarManager.playerChangeEvent(evType, description, player, data) - if evType == "newGroup" then - local theUnit = data.primeUnit - if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end - - csarManager.setCommsMenu(theUnit) -- allocates new config --- trigger.action.outText("+++csar: added " .. theUnit:getName() .. " to comms menu", 30) - return - end - - if evType == "removeGroup" then --- trigger.action.outText("+++csar: a group disappeared", 30) - local conf = csarManager.getConfigForUnitNamed(data.primeUnitName) - if conf then - csarManager.removeCommsFromConfig(conf) - end - return - end - - if evType == "leave" then - local conf = csarManager.getConfigForUnitNamed(player.unitName) - if conf then - csarManager.resetConfig(conf) - end - end - - if evType == "unit" then - -- player changed units. almost never in MP, but possible in solo - -- because of 1 seconds timing loop - -- will result in a new group appearing and a group disappearing, so we are good, - -- except we need to reset the conf so no troops are carried any longer - local conf = csarManager.getConfigForUnitNamed(data.oldUnitName) - if conf then - csarManager.resetConfig(conf) - end - end -end -- -- CSAR Bases @@ -1056,6 +913,10 @@ function csarManager.launchFlare(args) trigger.action.signalFlare(loc, color, 0) end + +-- WE ASSUME MISSIONS AREN'T TOO CLOSE TOGETHER TO +-- MESS UP MESSAGING OR PICKUP +-- if they are less than 2d apart, they can crosstalk each other function csarManager.update() -- every second -- schedule next invocation timer.scheduleFunction(csarManager.update, {}, timer.getTime() + 1/csarManager.ups) @@ -1065,147 +926,161 @@ function csarManager.update() -- every second -- now scan through all helo groups and see if they are close to a -- CSAR zone and initiate the help sequence - local allPlayerGroups = cfxPlayerGroups -- cfxPlayerGroups is a global, don't fuck with it! - -- contains per group a player record, use prime unit to access player's unit - for gname, pgroup in pairs(allPlayerGroups) do - local aUnit = pgroup.primeUnit -- get prime unit of that group - if aUnit:isExist() and aUnit:inAir() then -- exists and is flying +-- local allPlayerGroups = cfxPlayerGroups -- cfxPlayerGroups is a global, don't fuck with it! + local allPlayerUnits = dcsCommon.getAllExistingPlayersAndUnits() -- indexed by player name +--old -- contains per group a player record, use prime unit to access player's unit + for pname, aUnit in pairs(allPlayerUnits) do + if --aUnit:isExist() and + aUnit:inAir() and + dcsCommon.isTroopCarrier(aUnit, csarManager.troopCarriers) + then -- troop carrier and is flying local uPoint = aUnit:getPoint() local uName = aUnit:getName() local uGroup = aUnit:getGroup() local uID = uGroup:getID() local uSide = aUnit:getCoalition() local agl = dcsCommon.getUnitAGL(aUnit) - if dcsCommon.isTroopCarrier(aUnit, csarManager.troopCarriers) then - -- scan through all available csar missions to see if we are close - -- enough to trigger comms - for idx, csarMission in pairs (csarManager.openMissions) do - -- check if we are inside trigger range on the same side - local mp = cfxZones.getPoint(csarMission.zone, true) - local d = dcsCommon.distFlat(uPoint, mp) - if ((uSide == csarMission.side) or (csarMission.side == 0) ) - and (d < csarManager.rescueTriggerRange) then - -- we are in trigger distance. if we did not notify before - -- do it now - if not dcsCommon.arrayContainsString(csarMission.messagedUnits, uName) then - -- radio this unit with oclock and tell it they are in 2k range - -- also note if LZ is hot - local ownHeading = dcsCommon.getUnitHeadingDegrees(aUnit) - local oclock = dcsCommon.clockPositionOfARelativeToB(csarMission.zone.point, uPoint, ownHeading) .. " o'clock" - local msg = "\n" .. uName ..", " .. csarMission.name .. ". We can hear you, check your " .. oclock - if csarManager.useSmoke then msg = msg .. " - popping smoke" end - if csarManager.useFlare then - if csarManager.useSmoke then - msg = msg .. " and will launch flare in a few seconds" - else - msg = msg .. " - preparing flare" - end - -- schedule flare launch in 5-10 seconds - local args = {} - args.loc = mp - args.color = csarManager.flareColor - args.uID = uID - timer.scheduleFunction(csarManager.launchFlare, args, timer.getTime() + math.random(5)) - end - msg = msg .. "." - - if csarMission.isHot then - msg = msg .. " Be advised: LZ is hot." - end - msg = msg .. "\n" - trigger.action.outTextForGroup(uID, msg, 30) - trigger.action.outSoundForGroup(uID, csarManager.actionSound) -- "Quest Snare 3.wav") - table.insert(csarMission.messagedUnits, uName) -- remember that we messaged them so we don't do again - end - -- also pop smoke if not popped already, or more than 5 minutes ago - if csarManager.useSmoke and (timer.getTime() - csarMission.lastSmokeTime) >= 5 * 60 then - local smokePoint = dcsCommon.randomPointOnPerimeter( - csarManager.smokeDist, csarMission.zone.point.x, csarMission.zone.point.z) --cfxZones.createHeightCorrectedPoint(csarMission.zone.point) - -- trigger.action.smoke(smokePoint, 4 ) - dcsCommon.markPointWithSmoke(smokePoint, csarManager.smokeColor) - csarMission.lastSmokeTime = timer.getTime() - end - - -- now check if we are inside hover range and alt - -- in order to simultate winch ops - -- WARNING: WE ALWAYS ONLY CHECK A SINGLE UNIT - the first alive - local evacuee = csarMission.group:getUnit(1) - if evacuee then - local ep = evacuee:getPoint() - d = dcsCommon.distFlat(uPoint, ep) - d = math.floor(d * 10) / 10 - if d < csarManager.rescueTriggerRange * 0.5 then --csarManager.hoverRadius * 2 then - local ownHeading = dcsCommon.getUnitHeadingDegrees(aUnit) - local oclock = dcsCommon.clockPositionOfARelativeToB(ep, uPoint, ownHeading) .. " o'clock" - -- log distance - local hoverMsg = "Closing on " .. csarMission.name .. ", " .. d * 1 .. "m on your " .. oclock .. " o'clock" - - if d < csarManager.hoverRadius then - if (agl <= csarManager.hoverAlt) and (agl > 3) then - local hoverTime = csarMission.hoveringUnits[uName] - if not hoverTime then - -- create new entry - hoverTime = timer.getTime() - csarMission.hoveringUnits[uName] = timer.getTime() - end - hoverTime = timer.getTime() - hoverTime -- calculate number of seconds - local remainder = math.floor(csarManager.hoverDuration - hoverTime) - if remainder < 1 then remainder = 1 end - hoverMsg = "Steady... " .. d * 1 .. "m to your " .. oclock .. " o'clock, winching... (" .. remainder .. ")" - if hoverTime > csarManager.hoverDuration then - -- we rescued the guy! - hoverMsg = "We have " .. csarMission.name .. " safely on board!" - local conf = csarManager.getUnitConfig(aUnit) - csarManager.removeMission(csarMission) - table.insert(conf.troopsOnBoard, csarMission) - csarMission.group:destroy() -- will shut up radio as well - csarMission.group = nil - - -- now handle weight using cargoSuper - local theMassObject = cargoSuper.createMassObject( - csarManager.pilotWeight, - csarMission.name, - csarMission) - cargoSuper.addMassObjectTo( - uName, - "Evacuees", - theMassObject) - local totalMass = cargoSuper.calculateTotalMassFor(uName) - trigger.action.setUnitInternalCargo(uName, totalMass) - - if csarManager.verbose then - local allEvacuees = cargoSuper.getManifestFor(myName, "Evacuees") -- returns unlinked array - trigger.action.outText("+++csar: <" .. uName .. "> now has <" .. #allEvacuees .. "> groups of evacuees on board, totalling " .. totalMass .. "kg", 30) - end - - trigger.action.outTextForGroup(uID, hoverMsg, 30, true) - trigger.action.outSoundForGroup(uID, csarManager.actionSound) --"Quest Snare 3.wav") - - return -- we only ever rescue one - end -- hovered long enough - trigger.action.outTextForGroup(uID, hoverMsg, 30, true) - return -- only ever one winch op - else -- too high for hover - hoverMsg = "Evacuee " .. d * 1 .. "m on your " .. oclock .. " o'clock; land or descend to between 10 and 90 AGL for winching" - csarMission.hoveringUnits[uName] = nil -- reset timer - end - else -- not inside hover dist - -- remove the hover indicator for this - csarMission.hoveringUnits[uName] = nil - end - trigger.action.outTextForGroup(uID, hoverMsg, 30, true) - return -- only ever one winch op + local needsGC = false +-- local hasMessaged = false + for idx, csarMission in pairs (csarManager.openMissions) do + -- check if we are inside trigger range on the same side + local mp = cfxZones.getPoint(csarMission.zone, true) + local d = dcsCommon.distFlat(uPoint, mp) + if ((uSide == csarMission.side) or (csarMission.side == 0) ) + and (d < csarManager.rescueTriggerRange) then + -- we are in trigger distance. if we did not notify before + -- do it now, we ever only do this once for a unit for any mission + if not dcsCommon.arrayContainsString(csarMission.messagedUnits, uName) then + -- radio this unit with oclock and tell it they are in 2k range + -- also note if LZ is hot + local ownHeading = dcsCommon.getUnitHeadingDegrees(aUnit) + local oclock = dcsCommon.clockPositionOfARelativeToB(csarMission.zone.point, uPoint, ownHeading) .. " o'clock" + local msg = "\n" .. uName ..", " .. csarMission.name .. ". We can hear you, check your " .. oclock + if csarManager.useSmoke then msg = msg .. " - popping smoke" end + if csarManager.useFlare then + if csarManager.useSmoke then + msg = msg .. " and will launch flare in a few seconds" else - -- remove the hover indicator for this unit - csarMission.hoveringUnits[uName] = nil - end -- inside 2 * hover dist? - - end -- has evacuee - end -- if in range - end -- for all missions - end -- if troop carrier - end -- if exists - end -- for all players + msg = msg .. " - preparing flare" + end + -- schedule flare launch in 5-10 seconds + local args = {} + args.loc = mp + args.color = csarManager.flareColor + args.uID = uID + timer.scheduleFunction(csarManager.launchFlare, args, timer.getTime() + math.random(5)) + end + msg = msg .. "." + + if csarMission.isHot then + msg = msg .. " Be advised: LZ is hot." + end + msg = msg .. "\n" + trigger.action.outTextForGroup(uID, msg, 30) + trigger.action.outSoundForGroup(uID, csarManager.actionSound) -- "Quest Snare 3.wav") + table.insert(csarMission.messagedUnits, uName) -- remember that we messaged them so we don't do again + end + + -- also pop smoke if not popped already, or more than 5 minutes ago + if csarManager.useSmoke and (timer.getTime() - csarMission.lastSmokeTime) >= 5 * 60 then + local smokePoint = dcsCommon.randomPointOnPerimeter( + csarManager.smokeDist, csarMission.zone.point.x, csarMission.zone.point.z) + dcsCommon.markPointWithSmoke(smokePoint, csarManager.smokeColor) + csarMission.lastSmokeTime = timer.getTime() + end + + -- now check if we are inside hover range and alt + -- in order to simultate winch ops + -- if competition picked up, we skip this loop + local evacuee = nil + if csarMission.group then evacuee = csarMission.group:getUnit(1) end + if evacuee then + local ep = evacuee:getPoint() + d = dcsCommon.distFlat(uPoint, ep) + d = math.floor(d * 10) / 10 + if d < csarManager.rescueTriggerRange * 0.5 then + local ownHeading = dcsCommon.getUnitHeadingDegrees(aUnit) + local oclock = dcsCommon.clockPositionOfARelativeToB(ep, uPoint, ownHeading) .. " o'clock" + -- log distance + local hoverMsg = "Closing on " .. csarMission.name .. ", " .. d * 1 .. "m on your " .. oclock .. " o'clock" + + if d < csarManager.hoverRadius then + if (agl <= csarManager.hoverAlt) and (agl > 3) then + local hoverTime = csarMission.hoveringUnits[uName] + if not hoverTime then + -- create new entry + hoverTime = timer.getTime() + csarMission.hoveringUnits[uName] = timer.getTime() + end + hoverTime = timer.getTime() - hoverTime -- calculate number of seconds + local remainder = math.floor(csarManager.hoverDuration - hoverTime) + if remainder < 1 then remainder = 1 end + hoverMsg = "Steady... " .. d * 1 .. "m to your " .. oclock .. " o'clock, winching... (" .. remainder .. ")" + if hoverTime > csarManager.hoverDuration then + -- we rescued the guy! + hoverMsg = "We have " .. csarMission.name .. " safely on board!" + local conf = csarManager.getUnitConfig(aUnit) + -- mission now GC's after iteration csarManager.removeMission(csarMission) + table.insert(conf.troopsOnBoard, csarMission) + csarMission.group:destroy() -- will shut up radio as well + csarMission.group = nil -- no more evacuees + needsGC = true -- need filtering missions + + -- now handle weight using cargoSuper + local theMassObject = cargoSuper.createMassObject( + csarManager.pilotWeight, + csarMission.name, + csarMission) + cargoSuper.addMassObjectTo( + uName, + "Evacuees", + theMassObject) + local totalMass = cargoSuper.calculateTotalMassFor(uName) + trigger.action.setUnitInternalCargo(uName, totalMass) + + if csarManager.verbose then + local allEvacuees = cargoSuper.getManifestFor(myName, "Evacuees") -- returns unlinked array + trigger.action.outText("+++csar: <" .. uName .. "> now has <" .. #allEvacuees .. "> groups of evacuees on board, totalling " .. totalMass .. "kg", 30) + end + + --trigger.action.outTextForGroup(uID, hoverMsg, 30, true) + trigger.action.outSoundForGroup(uID, csarManager.actionSound) + + --return -- we only ever rescue one + end -- hovered long enough + --trigger.action.outTextForGroup(uID, hoverMsg, 30, true) + -- return -- only ever one winch op + else -- too high for hover + hoverMsg = "Evacuee " .. d * 1 .. "m on your " .. oclock .. " o'clock; land or descend to between 10 and 90 AGL for winching" + csarMission.hoveringUnits[uName] = nil -- reset timer + end + else -- not inside hover dist + -- remove the hover indicator for this + csarMission.hoveringUnits[uName] = nil + end + trigger.action.outTextForGroup(uID, hoverMsg, 30, true) + --return -- only ever one winch op + else + -- remove the hover indicator for this unit + csarMission.hoveringUnits[uName] = nil + end -- inside 2 * hover dist? + else + -- somebody snatched the evacuee + end -- if has evacuee + end -- if in range + end -- for all missions + -- now GC all missions if we lifted a pilot up (we no longer return after first succesful) + if needsGC then + local filtered = {} + for idx, csarMission in pairs(csarManager.openMissions) do + if csarMission.group then + table.insert(filtered, csarMission) + end + end + csarManager.openMissions = filtered + end + end -- if in Air + end -- for all player units -- now see and check if we need to spawn from a csar zone -- that has been told to spawn @@ -1216,32 +1091,37 @@ function csarManager.update() -- every second -- local currVal = theZone:getFlagValue(theZone.startCSAR) -- if currVal ~= theZone.lastCSARVal then if theZone:testZoneFlag(theZone.startCSAR, theZone.triggerMethod, "lastCSARVal") then - -- set up random point in zone - local mPoint = theZone:getPoint() - if theZone.rndLoc then mPoint = theZone:createRandomPointInZone() end - if theZone.onRoad then - mPoint.x, mPoint.z = land.getClosestPointOnRoads('roads',mPoint.x, mPoint.z) - end - local theMission = csarManager.createCSARMissionData( - mPoint, - theZone.csarSide, -- theSide - theZone.csarFreq, -- freq - theZone.csarName, -- name - theZone.numCrew, -- numCrew - theZone.timeLimit, -- timeLimit - theZone.csarMapMarker, -- mapMarker - 0.1, --theZone.radius) -- radius - nil) -- parashoo unit + local theMission = csarManager.createCSARMissionFromZone(theZone) csarManager.addMission(theMission) --theZone.lastCSARVal = currVal - if csarManager.verbose then - trigger.action.outText("+++csar: started CSAR mission " .. theZone.csarName, 30) + if csarManager.verbose or theZone.verbose then + trigger.action.outText("+++csar: started CSAR mission for <" .. theZone.csarName .. ">", 30) end end end end end +function csarManager.createCSARMissionFromZone(theZone) + -- set up random point in zone + local mPoint = theZone:getPoint() + if theZone.rndLoc then mPoint = theZone:createRandomPointInZone() end + if theZone.onRoad then + mPoint.x, mPoint.z = land.getClosestPointOnRoads('roads',mPoint.x, mPoint.z) + end + local theMission = csarManager.createCSARMissionData( + mPoint, + theZone.csarSide, -- theSide + theZone.csarFreq, -- freq + theZone.csarName, -- name + theZone.numCrew, -- numCrew + theZone.timeLimit, -- timeLimit + theZone.csarMapMarker, -- mapMarker + 0.1, --theZone.radius) -- radius + nil) -- parashoo unit + return theMission +end + -- -- create a CSAR Mission for a unit -- @@ -1412,9 +1292,6 @@ function csarManager.readConfigZone() csarManager.name = "csarManagerConfig" -- compat with cfxZones local theZone = cfxZones.getZoneByName("csarManagerConfig") if not theZone then - if csarManager.verbose then - trigger.action.outText("+++csar: NO config zone!", 30) - end theZone = cfxZones.createSimpleZone("csarManagerConfig") end csarManager.configZone = theZone -- save for flag banging compatibility @@ -1487,38 +1364,34 @@ function csarManager.start() -- read config csarManager.readConfigZone() - -- install callbacks for helo-relevant events - dcsCommon.addEventHandler(csarManager.somethingHappened, csarManager.preProcessor, csarManager.postProcessor) - - -- now iterate through all player groups and install the CSAR Menu - - local allPlayerGroups = cfxPlayerGroups -- cfxPlayerGroups is a global, don't fuck with it! - -- contains per group a player record, use prime unit to access player's unit - for gname, pgroup in pairs(allPlayerGroups) do - local aUnit = pgroup.primeUnit -- get prime unit of that group - csarManager.setCommsMenu(aUnit) - end - -- now install the new group notifier for new groups so we can remove and add CSAR menus - cfxPlayer.addMonitor(csarManager.playerChangeEvent) - -- now scan all zones that are CSAR drop-off for quick access csarManager.processCSARBASE() -- now scan all zones to create ME-placed CSAR missions -- and populate the available mission. csarManager.processCSARZones() + + -- install callbacks for helo-relevant events + --dcsCommon.addEventHandler(csarManager.somethingHappened, csarManager.preProcessor, csarManager.postProcessor) + world.addEventHandler(csarManager) + + -- now iterate through all player groups and install the CSAR Menu + local allPlayerUnits = dcsCommon.getAllExistingPlayerUnitsRaw() + for pName, aUnit in pairs(allPlayerUnits) do + csarManager.setCommsMenu(aUnit) + end - -- now call update so we can monitor progress of all helos, and alert them - -- when they are close to a CSAR + -- start updating and track all helicopters in the air against missions csarManager.update() -- say hi! - trigger.action.outText("cf/x CSAR v" .. csarManager.version .. " started", 30) + trigger.action.outText("cf/x CSAR Manager v" .. csarManager.version .. " started", 30) return true end -- let's get rolling if not csarManager.start() then + trigger.action.outText("cf/x CSAR Manager v" .. csarManager.version .. " FAILED to run", 30) csarManager = nil end @@ -1539,6 +1412,7 @@ end - when unloading one by menu, update weight!!! -- allow any airfied to be csarsafe by default, no longer *requires* csarbase + + -- minFreq, maxFreq settings for config and mission-individual - -- remove cfxPlayer dependency --]]-- \ No newline at end of file diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index c2b3c4f..d6e3b2c 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "3.0.0" +dcsCommon.version = "3.0.1" --[[-- VERSION HISTORY 3.0.0 - removed bad bug in stringStartsWith, only relevant if caseSensitive is false - point2text new intsOnly option @@ -8,6 +8,7 @@ dcsCommon.version = "3.0.0" - new pointInDirectionOfPointXYY() - createGroundGroupWithUnits now supports liveries - new getAllExistingPlayersAndUnits() +3.0.1 - clone: better handling of string type --]]-- -- dcsCommon is a library of common lua functions @@ -1187,6 +1188,9 @@ dcsCommon.version = "3.0.0" copy = tmp end end + elseif orig_type == "string" then + local tmp = "" + copy = tmp .. orig else -- number, string, boolean, etc copy = orig end diff --git a/modules/factoryZone.lua b/modules/factoryZone.lua index 95a2170..0f67610 100644 --- a/modules/factoryZone.lua +++ b/modules/factoryZone.lua @@ -1,5 +1,5 @@ factoryZone = {} -factoryZone.version = "3.0.0" +factoryZone.version = "3.1.0" factoryZone.verbose = false factoryZone.name = "factoryZone" @@ -11,6 +11,12 @@ factoryZone.name = "factoryZone" - use maxRadius from zone for spawning to support quad zones 3.0.0 - support for liveries via "factoryLiveries" zone - OOP dmlZones +3.1.0 - redD!, blueD! + - redP!, blueP! + - method + - productionTime config synonyme + - defendMe? attribute + - triggered 'shocked' mode via defendMe --]]-- factoryZone.requiredLibs = { @@ -104,16 +110,55 @@ function factoryZone.addFactoryZone(aZone) aZone.factoryTriggerMethod = aZone:getStringFromZoneProperty( "factoryTriggerMethod", "change") end + aZone.factoryMethod = aZone:getStringFromZoneProperty("factoryMethod", "inc") + if aZone:hasProperty("method") then + aZone.factoryMethod = aZone:getStringFromZoneProperty("method", "inc") + end + + if aZone:hasProperty("redP!") then + aZone.redP = aZone:getStringFromZoneProperty("redP!", "none") + end + if aZone.redP and aZone.attackersRED ~= "none" then + trigger.action.outText("***WARNING: factory <" .. aZone.name .. "> has RED production and uses 'redP!'", 30) + end + + if aZone:hasProperty("blueP!") then + aZone.blueP = aZone:getStringFromZoneProperty("blueP!", "none") + end + if aZone.blueP and aZone.attackersBLUE ~= "none" then + trigger.action.outText("***WARNING: factory <" .. aZone.name .. "> has BLUE production and uses 'blueP!'", 30) + end + + if aZone:hasProperty("redD!") then + aZone.redD = aZone:getStringFromZoneProperty("redD!", "none") + end + if aZone.redD and aZone.defendersRED ~= "none" then + trigger.action.outText("***WARNING: factory <" .. aZone.name .. "> has RED defenders and uses 'redD!'", 30) + end + + + if aZone:hasProperty("blueD!") then + aZone.blueD = aZone:getStringFromZoneProperty("blueD!", "none") + end + if aZone.blueD and aZone.defendersBLUE ~= "none" then + trigger.action.outText("***WARNING: factory <" .. aZone.name .. "> has BLUE defenders and uses 'blueD!'", 30) + end + + if aZone:hasProperty("defendMe?") then + aZone.defendMe = aZone:getStringFromZoneProperty("defendMe?", "none") + aZone.lastDefendMeValue = trigger.misc.getUserFlag(aZone.defendMe) + end + factoryZone.zones[aZone.name] = aZone factoryZone.verifyZone(aZone) end function factoryZone.verifyZone(aZone) -- do some sanity checks - if not cfxGroundTroops and (aZone.attackersRED ~= "none" or aZone.attackersBLUE ~= "none") then - trigger.action.outText("+++factZ: " .. aZone.name .. " attackers need cfxGroundTroops to function", 30) - end - +-- if not cfxGroundTroops and (aZone.attackersRED ~= "none" or aZone.attackersBLUE ~= "none") then + -- now can also bang on flags, no more verification + -- unless we want to beef them up +-- end end function factoryZone.spawnAttackTroops(theTypes, aZone, aCoalition, aFormation) @@ -183,6 +228,7 @@ end -- function factoryZone.sendOutAttackers(aZone) + -- sanity check: never done for neutral zones if aZone.owner == 0 then if aZone.verbose or factoryZone.verbose then @@ -193,16 +239,31 @@ function factoryZone.sendOutAttackers(aZone) -- only spawn if there are zones to attack if not cfxOwnedZones.enemiesRemaining(aZone) then - if factoryZone.verbose then + if aZone.verbose or factoryZone.verbose then trigger.action.outText("+++factZ - no enemies, resting ".. aZone.name, 30) end return end - if factoryZone.verbose then + if factoryZone.verbose or aZone.verbose then trigger.action.outText("+++factZ - attack cycle for ".. aZone.name, 30) end + -- bang on xxxP! + if aZone.owner == 1 and aZone.redP then + if aZone.verbose or factoryZone.verbose then + trigger.action.outText("+++factZ: polling redP! <" .. aZone.redP .. "> for factrory <" .. aZone.name .. ">") + end + aZone:pollFlag(aZone.redP, aZone.factoryMethod) + end + + if aZone.owner == 2 and aZone.blueP then + if aZone.verbose or factoryZone.verbose then + trigger.action.outText("+++factZ: polling blueP! <" .. aZone.blueP .. "> for factrory <" .. aZone.name .. ">") + end + aZone:pollFlag(aZone.blueP, aZone.factoryMethod) + end + -- step one: get the attackers local attackers = aZone.attackersRED; if (aZone.owner == 2) then attackers = aZone.attackersBLUE end @@ -248,8 +309,23 @@ function factoryZone.repairDefenders(aZone) if (aZone.owner == 2) then defenders = aZone.defendersBLUE end local unitTypes = {} -- build type names - -- if none, we are done - if defenders == "none" then return end + -- if none, we are done, save for the outputs + if (not defenders) or (defenders == "none") then + if aZone.owner == 1 and aZone.redD then + if aZone.verbose or factoryZone.verbose then + trigger.action.outText("+++factZ: polling redD! <" .. aZone.redD .. "> for repair factory <" .. aZone.name .. ">", 30) + end + aZone:pollFlag(aZone.redD, aZone.factoryMethod) + end + + if aZone.owner == 2 and aZone.blueD then + if aZone.verbose or factoryZone.verbose then + trigger.action.outText("+++factZ: polling blueD! <" .. aZone.blueD .. "> for repair factory <" .. aZone.name .. ">", 30) + end + aZone:pollFlag(aZone.blueD, aZone.factoryMethod) + end + return + end -- split theTypes into an array of types allTypes = dcsCommon.trimArray( @@ -308,14 +384,32 @@ end function factoryZone.spawnDefenders(aZone) -- sanity check: never done for non-neutral zones + if aZone.verbose or factoryZone.verbose then + trigger.action.outText("+++factZ: starting defender cycle for <" .. aZone.name .. ">", 30) + end + if aZone.owner == 0 then if aZone.verbose or factoryZone.verbose then trigger.action.outText("+++factZ: spawnDefenders invoked for NEUTRAL zone <" .. aZone.name .. ">", 30) end return end - + + -- bang! on xxxD! local defenders = aZone.defendersRED; + if aZone.owner == 1 and aZone.redD then + if aZone.verbose or factoryZone.verbose then + trigger.action.outText("+++factZ: polling redD! <" .. aZone.redD .. "> for factrory <" .. aZone.name .. ">", 30) + end + aZone:pollFlag(aZone.redD, aZone.factoryMethod) + end + + if aZone.owner == 2 and aZone.blueD then + if aZone.verbose or factoryZone.verbose then + trigger.action.outText("+++factZ: polling blueD! <" .. aZone.blueD .. "> for factory <" .. aZone.name .. ">", 30) + end + aZone:pollFlag(aZone.blueD, aZone.factoryMethod) + end if (aZone.owner == 2) then defenders = aZone.defendersBLUE end -- before we spawn new defenders, remove the old ones @@ -388,7 +482,7 @@ function factoryZone.updateZoneProduction(aZone) aZone.defenders then -- we have defenders if aZone.defenders:isExist() then - -- isee if group was damaged + -- see if group was damaged if not aZone.lastDefenders then -- fresh group, probably from persistence, needs init aZone.lastDefenders = -1 @@ -458,7 +552,7 @@ function factoryZone.updateZoneProduction(aZone) if timer.getTime() > aZone.timeStamp + factoryZone.repairTime then aZone.timeStamp = timer.getTime() -- wait's up, repair one defender, then check if full strength - factoryZone.repairDefenders(aZone) + factoryZone.repairDefenders(aZone) -- will also bang on redD and blueD if present -- see if we are full strenght and if so go to attack, else set timer to reair the next unit if aZone.defenders and aZone.defenders:isExist() and aZone.defenders:getSize() >= aZone.defenders:getInitialSize() then -- we are at max size, time to produce some attackers @@ -467,7 +561,14 @@ function factoryZone.updateZoneProduction(aZone) aZone.timeStamp = timer.getTime() if factoryZone.verbose then trigger.action.outText("+++factZ: State " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name, 30) - end + end + elseif (aZone.redD or aZone.blueD) then + -- we start attacking cycle for out signal + nextState = "attacking" + aZone.timeStamp = timer.getTime() + if factoryZone.verbose then + trigger.action.outText("+++factZ: progessing tate " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name .. " for redD/blueD", 30) + end end end @@ -540,6 +641,19 @@ function factoryZone.update() if theZone.activateFlag and cfxZones.testZoneFlag(theZone, theZone.activateFlag, theZone.factoryTriggerMethod, "lastActivateValue") then theZone.paused = false end + + -- see if zone external defendMe was polled to bring it to + -- shoked state + if theZone.defendMe and theZone:testZoneFlag(theZone.defendMe, theZone.factoryTriggerMethod, "lastDefendMeValue") then + if theZone.verbose or factoryZone.verbose then + trigger.action.outText("+++factZ: setting factory <" .. theZone.name .. "> to shocked/produce defender mode", 30) + end + theZone.state = "shocked" + theZone.timeStamp = timer.getTime() + theZone.lastDefenders = 0 + theZone.defenders = nil -- nil, but no delete! + end + -- do production for this zone factoryZone.updateZoneProduction(theZone) end -- iterating all zones @@ -677,6 +791,9 @@ function factoryZone.readConfigZone(theZone) factoryZone.verbose = theZone.verbose factoryZone.defendingTime = theZone:getNumberFromZoneProperty( "defendingTime", 100) factoryZone.attackingTime = theZone:getNumberFromZoneProperty( "attackingTime", 300) + if theZone:hasProperty("productionTime") then + factoryZone.attackingTime = theZone:getNumberFromZoneProperty( "productionTime", 300) + end factoryZone.shockTime = theZone:getNumberFromZoneProperty("shockTime", 200) factoryZone.repairTime = theZone:getNumberFromZoneProperty( "repairTime", 200) factoryZone.targetZones = "OWNED" diff --git a/modules/objectDestructDetector.lua b/modules/objectDestructDetector.lua index 89f4a2a..c9f0974 100644 --- a/modules/objectDestructDetector.lua +++ b/modules/objectDestructDetector.lua @@ -1,5 +1,5 @@ cfxObjectDestructDetector = {} -cfxObjectDestructDetector.version = "2.0.0" +cfxObjectDestructDetector.version = "2.0.2" cfxObjectDestructDetector.verbose = false cfxObjectDestructDetector.requiredLibs = { "dcsCommon", -- always @@ -18,6 +18,10 @@ cfxObjectDestructDetector.requiredLibs = { ID changes (happens with map updates) fail addZone when name property is missing 2.0.1 check that the object is within the zone onEvent + 2.0.2 redScore and bluescore attributes + API for PlayerScore to pass back redScore/blueScore + if objects was killed by player + verbosity bug fixed after kill (ref to old ID) --]]-- cfxObjectDestructDetector.objectZones = {} @@ -78,8 +82,59 @@ function cfxObjectDestructDetector.processObjectDestructZone(aZone) elseif aZone:hasProperty("objectDestroyed!") then aZone.outDestroyFlag = aZone:getStringFromZoneProperty( "objectDestroyed!", "*none") end + + --PlayerScore interface (data) + if aZone:hasProperty("redScore") then + aZone.redScore = aZone:getNumberFromZoneProperty("redScore", 0) +-- if aZone.verbose then +-- trigger.action.outText("") +-- end + end + + if aZone:hasProperty("blueScore") then + aZone.blueScore = aZone:getNumberFromZoneProperty("blueScore", 0) + end + return true end + +-- +-- Interface with PlayerScore +-- ========================== +-- PlayerScore invokes us when it finds a scenery object was killed +-- we check if it is one of ours, and if so, if a score is attached +-- for that side +function cfxObjectDestructDetector.playerScoreForKill(theObject, killSide) + if not theObject then return nil end + if not killSide then return nil end + local pos = theObject:getPoint() + local desc = theObject:getDesc() + if not desc then return nil end + desc = desc.typeName -- deref type name to match zone objName + if not desc then return end + for idx, theZone in pairs (cfxObjectDestructDetector.objectZones) do + -- see if we can find a matching ODD + if (not theZone.isDestroyed) -- make sure it's not a dupe + and theZone.objName == desc + and theZone:pointInZone(pos) + then + -- yes, ODD tracks this object + if cfxObjectDestructDetector.verbose or theZone.verbose then + trigger.action.outText("OOD: score invocation for hit scenery object <" .. desc .. ">, tracked with <" .. theZone.name .. ">", 30) + end + if killSide == 1 then return theZone.redScore end -- can be nil! + if killSide == 2 then return theZone.blueScore end + -- if we get here, the object is tracked, but has no + -- playerScore attached. simply exist with nil + if cfxObjectDestructDetector.verbose or theZone.verbose then + trigger.action.outText("OOD: scenery object <" .. desc .. ">, tracked but no player score defined for coa <" .. killSide .. ">.", 30) + end + return nil + end + end + return nil +end + -- -- ON EVENT -- @@ -106,7 +161,7 @@ function cfxObjectDestructDetector:onEvent(event) -- invoke callbacks cfxObjectDestructDetector.invokeCallbacksFor(aZone) if aZone.verbose or cfxObjectDestructDetector.verbose then - trigger.action.outText("OBJECT KILL: " .. id, 30) + trigger.action.outText("OBJECT KILL: " .. matchMe .. " for odd <" .. aZone.name .. ">", 30) end -- save state for persistence aZone.isDestroyed = true diff --git a/modules/playerScore.lua b/modules/playerScore.lua index 8801727..77c97fe 100644 --- a/modules/playerScore.lua +++ b/modules/playerScore.lua @@ -12,6 +12,7 @@ cfxPlayerScore.firstSave = true -- to force overwrite - sceneryObject detection improvements - DCS 2.9 safe 3.0.1 - cleanup + 3.0.2 - interface with ObjectDestructDetector for scoring scenery objects --]]-- @@ -208,8 +209,9 @@ function cfxPlayerScore.cat2BaseScore(inCat) return 1 end -function cfxPlayerScore.object2score(inVictim) -- does not have group - if not inVictim then return 0 end +function cfxPlayerScore.object2score(inVictim, killSide) -- does not have group + if not inVictim then return 0 end + if not killSide then killSide = -1 end local inName = inVictim:getName() if dcsCommon.isSceneryObject(inVictim) then local desc = inVictim:getDesc() @@ -217,6 +219,12 @@ function cfxPlayerScore.object2score(inVictim) -- does not have group -- same as object destruct detector to -- avoid ID changes inName = desc.typeName + if cfxObjectDestructDetector then + -- ask ODD if it knows the object and what score was + -- awarded for a kill from that side + local objectScore = cfxObjectDestructDetector.playerScoreForKill(inVictim, killSide) + if objectScore then return objectScore end + end end if not inName then return 0 end if type(inName) == "number" then @@ -744,7 +752,7 @@ function cfxPlayerScore.killDetected(theEvent) if wasBuilding then -- these objects have no coalition; we simply award the score if -- it exists in look-up table. - local staticScore = cfxPlayerScore.object2score(victim) + local staticScore = cfxPlayerScore.object2score(victim, killSide) if staticScore > 0 then trigger.action.outSoundForCoalition(killSide, cfxPlayerScore.scoreSound) cfxPlayerScore.awardScoreTo(killSide, staticScore, killerName) @@ -754,8 +762,10 @@ function cfxPlayerScore.killDetected(theEvent) end -- was it fratricide? + -- if we get here, it CANT be a scenery object + -- but can be a static object, and stO have a coalition local vicSide = victim:getCoalition() - local fraternicide = killSide == vicSide + local fraternicide = (killSide == vicSide) local vicDesc = victim:getTypeName() local scoreMod = 1 -- start at one @@ -767,7 +777,7 @@ function cfxPlayerScore.killDetected(theEvent) -- static objects have no group local staticName = victim:getName() -- on statics, this returns -- name as entered in TOP LINE - local staticScore = cfxPlayerScore.object2score(victim) + local staticScore = cfxPlayerScore.object2score(victim, killSide) if staticScore > 0 then -- this was a named static, return the score - unless our own @@ -1488,6 +1498,7 @@ score zones - zones outside of which no scoring counts, but feats are still ok - add take off feats +- integrate with objectDestructDetector can be extended with other, standalone feat modules that follow the same pattern, e.g. enter a zone, detect someone diff --git a/modules/theDebugger.lua b/modules/theDebugger.lua index ee8e0ce..78530fd 100644 --- a/modules/theDebugger.lua +++ b/modules/theDebugger.lua @@ -1,8 +1,8 @@ --- theDebugger +-- theDebugger 2.x debugger = {} -debugger.version = "2.0.0" +debugger.version = "2.1.0" debugDemon = {} -debugDemon.version = "2.0.0" +debugDemon.version = "2.1.0" debugger.verbose = false debugger.ups = 4 -- every 0.25 second @@ -33,6 +33,13 @@ debugger.log = "" - debuggerSpawnTypes zone - reading debuggerSpawnTypes - removed some silly bugs / inconsistencies + 2.1.0 - debugging code is now invoked deferred to avoid + DCS crash after exiting. Debug code now executes + outside of the event code's bracket. + debug invocation on clone of data structure + readback verification of flag set + fixed getProperty() in debugger with zone + --]]-- debugger.requiredLibs = { @@ -201,7 +208,7 @@ end function debugger.createDebuggerWithZone(theZone) -- watchflag input trigger theZone.debugInputMethod = theZone:getStringFromZoneProperty( "triggerMethod", "change") - if theZone.hasProperty("debugTriggerMethod") then + if theZone:hasProperty("debugTriggerMethod") then theZone.debugInputMethod = theZone:getStringFromZoneProperty("debugTriggerMethod", "change") elseif theZone:hasProperty("inputMethod") then theZone.debugInputMethod = theZone:getStringFromZoneProperty(theZone, "inputMethod", "change") @@ -644,7 +651,8 @@ debugDemon.splitDelimiter = " " debugDemon.commandTable = {} -- key, value pair for command processing per keyword debugDemon.keepOpen = false -- keep mark open after a successful command debugDemon.snapshot = {} - +debugDemon.activeIdx = -1 -- to detect if a window was close + -- and prevent execution of debugger function debugDemon.hasMark(theString) -- check if the string begins with the sequece to identify commands if not theString then return false end @@ -676,6 +684,7 @@ 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 @@ -683,12 +692,19 @@ function debugDemon:onEvent(theEvent) -- else an error is displayed and the mark remains. if debugDemon.hasMark(theEvent.text) then -- strip the mark - local commandString = theEvent.text:sub(1+debugDemon.markOfDemon:len()) + local cCommand = dcsCommon.clone(theEvent.text, true) + 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 success = debugDemon.executeCommand(commands, theEvent) + local cTheEvent = dcsCommon.clone(theEvent, true) -- 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 @@ -696,13 +712,35 @@ function debugDemon:onEvent(theEvent) 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 + + -- remove this mark after successful execution + if success then + trigger.action.removeMark(cTheEvent.idx) + debugDemon.activeIdx = nil + else + -- we could play some error sound + end +end + -- -- add / remove commands to/from vocabulary -- @@ -1103,6 +1141,11 @@ function debugDemon.processSetCommand(args, event) debugger.outText("*** [" .. dcsCommon.nowString() .. "] debug: set flag <" .. theName .. "> to <" .. theVal .. ">" .. note, 30) + local newVal = trigger.misc.getUserFlag(theName) + if theVal ~= newVal then + debugger.outText("*** [" .. dcsCommon.nowString() .. "] debug: readback failure for flag <" .. theName .. ">: expected <" .. theVal .. ">, got <" .. newVal .. "!", 30) + end + return true end diff --git a/modules/williePete.lua b/modules/williePete.lua index 3c05c2f..6c5fb51 100644 --- a/modules/williePete.lua +++ b/modules/williePete.lua @@ -1,5 +1,5 @@ williePete = {} -williePete.version = "2.0.0" +williePete.version = "2.0.2" williePete.ups = 10 -- we update at 10 fps, so accuracy of a -- missile moving at Mach 2 is within 33 meters, -- with interpolation even at 3 meters @@ -17,6 +17,8 @@ williePete.requiredLibs = { 2.0.0 - dmlZones, OOP - Guards for multi-unit player groups - getFirstLivingPlayerInGroupNamed() + 2.0.1 - added Harrier's FFAR M156 WP + 2.0.2 - hardened playerUpdate() --]]-- williePete.willies = {} @@ -28,7 +30,7 @@ williePete.blastedObjects = {} -- used when we detonate something -- recognizes WP munitions. May require regular update when new -- models come out. -williePete.smokeWeapons = {"HYDRA_70_M274","HYDRA_70_MK61","HYDRA_70_MK1","HYDRA_70_WTU1B","HYDRA_70_M156","HYDRA_70_M158","BDU_45B","BDU_33","BDU_45","BDU_45LGB","BDU_50HD","BDU_50LD","BDU_50LGB","C_8CM", "SNEB_TYPE254_H1_GREEN", "SNEB_TYPE254_H1_RED", "SNEB_TYPE254_H1_YELLOW"} +williePete.smokeWeapons = {"HYDRA_70_M274","HYDRA_70_MK61","HYDRA_70_MK1","HYDRA_70_WTU1B","HYDRA_70_M156","HYDRA_70_M158","BDU_45B","BDU_33","BDU_45","BDU_45LGB","BDU_50HD","BDU_50LD","BDU_50LGB","C_8CM", "SNEB_TYPE254_H1_GREEN", "SNEB_TYPE254_H1_RED", "SNEB_TYPE254_H1_YELLOW", "FFAR M156 WP"} function williePete.addWillie(theWillie) table.insert(williePete.willies, theWillie) @@ -617,19 +619,23 @@ function williePete.playerUpdate() -- make sure at least one unit still exists local dropUnit = true local theGroup = Group.getByName(unitInfo.gName) - local allUnits = theGroup:getUnits() - for idx, theUnit in pairs(allUnits) do - --local theUnit = Unit.getByName(unitInfo.name) - if theUnit and Unit.isExist(theUnit) and - theUnit.getPlayerName and theUnit:getPlayerName() then - local up = theUnit:getPoint() - up.y = 0 - local isInside, dist = cfxZones.isPointInsideZone(up, theZone, theZone.checkInRange) - - if isInside then - dropUnit = false + if theGroup then + local allUnits = theGroup:getUnits() + for idx, theUnit in pairs(allUnits) do + --local theUnit = Unit.getByName(unitInfo.name) + if theUnit and Unit.isExist(theUnit) and + theUnit.getPlayerName and theUnit:getPlayerName() then + local up = theUnit:getPoint() + up.y = 0 + local isInside, dist = cfxZones.isPointInsideZone(up, theZone, theZone.checkInRange) + + if isInside then + dropUnit = false + end end end + else + trigger.action.outText("+++wp: strange issues with group <" .. gName .. ">, does not exist. Skipped in playerUpdate()", 30) end if dropUnit then -- all outside, remove from zone check-in diff --git a/tutorial & demo missions/demo - Attack of the CloneZ.miz b/tutorial & demo missions/demo - Attack of the CloneZ.miz index 5266c68..6440729 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 - Bug Hunt.miz b/tutorial & demo missions/demo - Bug Hunt.miz index aa2c299..3bb6e7d 100644 Binary files a/tutorial & demo missions/demo - Bug Hunt.miz and b/tutorial & demo missions/demo - Bug Hunt.miz differ diff --git a/tutorial & demo missions/demo - CSAR of Georgia.miz b/tutorial & demo missions/demo - CSAR of Georgia.miz index ec9cc5a..5ccba76 100644 Binary files a/tutorial & demo missions/demo - CSAR of Georgia.miz and b/tutorial & demo missions/demo - CSAR of Georgia.miz differ diff --git a/tutorial & demo missions/demo - Clone Factory.miz b/tutorial & demo missions/demo - Clone Factory.miz new file mode 100644 index 0000000..acf8a40 Binary files /dev/null and b/tutorial & demo missions/demo - Clone Factory.miz differ diff --git a/tutorial & demo missions/demo - Landing Lessons.miz b/tutorial & demo missions/demo - Landing Lessons.miz index 2118c0d..5f5bf51 100644 Binary files a/tutorial & demo missions/demo - Landing Lessons.miz and b/tutorial & demo missions/demo - Landing Lessons.miz differ diff --git a/tutorial & demo missions/demo - airbase mine.miz b/tutorial & demo missions/demo - airbase mine.miz index 4f98756..c11301f 100644 Binary files a/tutorial & demo missions/demo - airbase mine.miz and b/tutorial & demo missions/demo - airbase mine.miz differ diff --git a/tutorial & demo missions/demo - debug events and more.miz b/tutorial & demo missions/demo - debug events and more.miz index 2cd3aa7..5b31985 100644 Binary files a/tutorial & demo missions/demo - debug events and more.miz and b/tutorial & demo missions/demo - debug events and more.miz differ