diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 467630b..7593082 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 2ccf642..9f01367 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/FARPZones.lua b/modules/FARPZones.lua index 05e348a..90733ae 100644 --- a/modules/FARPZones.lua +++ b/modules/FARPZones.lua @@ -1,5 +1,5 @@ FARPZones = {} -FARPZones.version = "2.1.1" +FARPZones.version = "2.2.0" FARPZones.verbose = false --[[-- Version History @@ -24,7 +24,8 @@ FARPZones.verbose = false 2.0.2 - clean-up verbosity enhancements 2.1.0 - integration with camp: needs repairs, produceResourceVehicles() - 2.1.1 - loading a farp from data respaws all defenders and resource vehicles + 2.1.1 - loading a farp from data respaws all defenders and resource vehicles + 2.2.0 - changing a FARP's owner invokes SSBClient if it is loaded --]]-- @@ -331,9 +332,7 @@ end function FARPZones.produceVehicles(theFarp) local theZone = theFarp.zone --- trigger.action.outText("entering veh prod run for farp zone <" .. theZone.name .. ">, owner is <" .. theFarp.owner .. ">", 30) - --end - + -- abort production if farp is owned by neutral and -- neutralproduction is false if theFarp.owner == 0 and not theFarp.neutralProduction then @@ -382,18 +381,7 @@ function FARPZones.produceVehicles(theFarp) -- spawn resource vehicles FARPZones.produceResourceVehicles(theFarp, theCoalition) ---[[-- - unitTypes = FARPZones.resourceTypes - local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( - theCoalition, - theFarp.name .. "-R" .. theFarp.count, -- must be unique - theFarp.resZone, - unitTypes, - "line_v", - theFarp.resHeading) - theFarp.resources = theGroup - theFarp.resourceData = theData ---]]-- + -- update unique counter theFarp.count = theFarp.count + 1 end @@ -508,7 +496,6 @@ function FARPZones.saveData() -- iterate all farp data and put them into a container each for theZone, theFARP in pairs(FARPZones.allFARPZones) do fName = theZone.name - --trigger.action.outText("frpZ persistence: processing FARP <" .. fName .. ">", 30) local fData = {} fData.owner = theFARP.owner fData.defenderData = dcsCommon.clone(theFARP.defenderData) @@ -528,6 +515,17 @@ function FARPZones.saveData() return theData end +function FARPZones.delayedSSB() + -- invoke SSBClient to re-scan all airfields + -- if it is loaded in the mission + if FARPZones.verbose then + trigger.action.outText("FARPz: delayed SSB invocation", 30) + end + if cfxSSBClient then + cfxSSBClient.setSlotAccessByAirfieldOwner() + end +end + function FARPZones.loadMission() local theData = persistence.getSavedDataForModule("FARPZones") if not theData then @@ -548,27 +546,9 @@ function FARPZones.loadMission() theAB:setCoalition(theFARP.owner) -- FARP is in lockup. theFARP.defenderData = dcsCommon.clone(fData.defenderData) - --[[-- - local groupData = fData.defenderData - if groupData and #groupData.units > 0 then - local cty = groupData.cty - local cat = groupData.cat - theFARP.defenders = coalition.addGroup(cty, cat, groupData) - end - - groupData = fData.resourceData - if groupData and #groupData.units > 0 then - local cty = groupData.cty - local cat = groupData.cat - theFARP.resources = coalition.addGroup(cty, cat, groupData) - end - --]]-- FARPZones.produceVehicles(theFARP) -- do full defender and resource cycle FARPZones.drawFARPCircleInMap(theFARP) -- mark in map --- if (not theFARP.defenders) and (not theFARP.resources) then - -- we instigate a resource and defender drop --- FARPZones.produceVehicles(theFARP) --- end + else trigger.action.outText("frpZ: persistence: FARP <" .. fName .. "> no longer exists in mission, skipping", 30) end @@ -580,10 +560,8 @@ end -- Start -- function FARPZones.releaseFARPS() --- trigger.action.outText("Releasing hold on FARPS", 30) for idx, aFarp in pairs(FARPZones.lockup) do aFarp:autoCapture(true) --- trigger.action.outText("releasing farp <" .. aFarp:getName() .. ">", 30) end end @@ -592,13 +570,10 @@ function FARPZones.readConfig() if not theZone then theZone = cfxZones.createSimpleZone("farpZonesConfig") end - FARPZones.verbose = theZone.verbose - FARPZones.spinUpDelay = theZone:getNumberFromZoneProperty( "spinUpDelay", 30) FARPZones.refresh = theZone:getNumberFromZoneProperty("refresh", -1) - end @@ -635,8 +610,6 @@ function FARPZones.start() for k, aZone in pairs(theZones) do local aFARP = FARPZones.createFARPFromZone(aZone) -- read attributes from DCS FARPZones.addFARPZone(aFARP) -- add to managed zones - -- moved FARPZones.drawFARPCircleInMap(aFARP) -- mark in map - -- moved FARPZones.produceVehicles(aFARP) -- allocate initial vehicles if FARPZones.verbose then trigger.action.outText("processed FARP <" .. aZone.name .. "> now owned by " .. aZone.owner, 30) end @@ -656,6 +629,7 @@ function FARPZones.start() FARPZones.startingUp = false -- not needed / read anywhere timer.scheduleFunction(FARPZones.releaseFARPS, {}, timer.getTime() + 5) + timer.scheduleFunction(FARPZones.delayedSSB, {}, timer.getTime() + 10) if FARPZones.refresh > 0 then timer.scheduleFunction(FARPZones.refreshMap, {}, timer.getTime() + FARPZones.refresh) diff --git a/modules/RNDFlags.lua b/modules/RNDFlags.lua index 79bbc74..a1bd833 100644 --- a/modules/RNDFlags.lua +++ b/modules/RNDFlags.lua @@ -75,13 +75,9 @@ function rndFlags.createRNDWithZone(theZone) -- trigger flag if theZone:hasProperty("f?") then theZone.triggerFlag = theZone:getStringFromZoneProperty("f?", "none") - end - - if theZone:hasProperty("in?") then + elseif theZone:hasProperty("in?") then theZone.triggerFlag = theZone:getStringFromZoneProperty("in?", "none") - end - - if theZone:hasProperty("rndPoll?") then + elseif theZone:hasProperty("rndPoll?") then theZone.triggerFlag = theZone:getStringFromZoneProperty("rndPoll?", "none") end diff --git a/modules/SSBClient.lua b/modules/SSBClient.lua index 9d14230..dd01396 100644 --- a/modules/SSBClient.lua +++ b/modules/SSBClient.lua @@ -1,5 +1,5 @@ cfxSSBClient = {} -cfxSSBClient.version = "4.0.1" +cfxSSBClient.version = "5.0.0" cfxSSBClient.verbose = false cfxSSBClient.singleUse = false -- set to true to block crashed planes -- NOTE: singleUse (true) requires SSB to disable immediate respawn after kick @@ -8,7 +8,7 @@ cfxSSBClient.reUseAfter = -1 -- seconds for re-use delay cfxSSBClient.requiredLibs = { "dcsCommon", -- always - "cfxMX", --"cfxGroups", -- for slot access + "cfxMX", -- for ME Player data access "cfxZones", -- Zones, of course } @@ -17,7 +17,13 @@ Version History 4.0.0 - dmlZones - cfxMX instead of cfxGroups 4.0.1 - check slot availability immediately upon start - - ssb autoenable option + - ssb auto-enable option + 5.0.0 - re-write: support for DCS new dynamic spawns + - work-around for DCS bug that passes dead / deallocated objects + - SINGLE-USE and dynamic spawns are mutually exclusive + as single-use cannot cover dynamic slots, so authors must + disable or limit the amount of planes manually + --]]-- cfxSSBClient.enabledFlagValue = 0 -- DO NOT CHANGE, MUST MATCH SSB @@ -36,9 +42,9 @@ cfxSSBClient.slotActions = { cfxSSBClient.keepInAirGroups = false -- if false we only look at planes starting on the ground -- setting this to true only makes sense if you plan to bind in-air starts to airfields -cfxSSBClient.playerGroups = {} -cfxSSBClient.closedAirfields = {} -- list that closes airfields for any aircrafts -cfxSSBClient.playerPlanes = {} -- names of units that a player is flying +cfxSSBClient.playerGroups = {} -- indexed by groupName. group data with .airfield attribute +cfxSSBClient.closedAirfields = {} -- list that closes airfields for all aircrafts +cfxSSBClient.playerPlanes = {} -- names of unit that a player is flying indexed by unit name cfxSSBClient.crashedGroups = {} -- names of groups to block after crash of their player-flown plane cfxSSBClient.slotState = {} -- keeps a record of which slot has which value. For persistence and debugging cfxSSBClient.occupiedUnits = {} -- by unit name if occupied to prevent kicking. clears after crash or leaving plane @@ -154,6 +160,7 @@ end function cfxSSBClient.setSlotAccessForGroup(theGroup) if not theGroup then return end -- WARNING: theGroup is cfxGroup record + -- amended for dynamic groups local theName = theGroup.name -- we now check if any plane of that group is still @@ -163,15 +170,27 @@ function cfxSSBClient.setSlotAccessForGroup(theGroup) -- we now iterate all playerUnits in theGroup. -- theGroup is cfxGroup - for idx, playerData in pairs (theGroup.playerUnits) do - local uName = playerData.name - if cfxSSBClient.occupiedUnits[uName] then + if theGroup.playerUnits then + -- this is a ME unit + for idx, playerData in pairs (theGroup.playerUnits) do + local uName = playerData.name + if cfxSSBClient.occupiedUnits[uName] then + if cfxSSBClient.verbose then + trigger.action.outText("+++ssbc: unit <" .. uName .. "> of group <" .. theName .. "> is occupied, no airfield check", 30) + end + return + end + end + else + -- this is a dynamic unit + if cfxSSBClient.occupiedUnits[theGroup.uName] then + local uName = theGroup.uName if cfxSSBClient.verbose then - trigger.action.outText("+++ssbc: unit <" .. uName .. "> of group <" .. theName .. "> is occupied, no airfield check", 30) + trigger.action.outText("+++ssbc: DYNAMIC unit <" .. uName .. "> of group <" .. theName .. "> is occupied, no airfield check", 30) end return end - end + end -- when we get here, no unit in the entire group is occupied local theMatchingAirfield = theGroup.airfield @@ -230,8 +249,6 @@ function cfxSSBClient.setSlotAccessForGroup(theGroup) end trigger.action.setUserFlag(theName, blockState) cfxSSBClient.slotState[theName] = blockState - --if cfxSSBClient.verbose then - --end else if cfxSSBClient.verbose then trigger.action.outText("+++SSB: group ".. theName .. " no bound airfield: available", 30) @@ -252,18 +269,22 @@ function cfxSSBClient.setSlotAccessForUnit(theUnit) -- calls setSlotAccessForGro end function cfxSSBClient.getPlayerGroupForGroupNamed(aName) + --is now indexed !! + return cfxSSBClient.playerGroups[aName] + --[[-- local pGroups = cfxSSBClient.playerGroups for idx, theGroup in pairs(pGroups) do if theGroup.name == aName then return theGroup end end - return nil + return nil +--]]-- end function cfxSSBClient.setSlotAccessByAirfieldOwner() -- get all groups that have a player-controlled aircraft -- now uses cached, reduced set of player planes - local pGroups = cfxSSBClient.playerGroups - for idx, theGroup in pairs(pGroups) do + local pGroups = cfxSSBClient.playerGroups -- indexed by name + for gName, theGroup in pairs(pGroups) do cfxSSBClient.setSlotAccessForGroup(theGroup) end end @@ -294,14 +315,8 @@ function cfxSSBClient:onEvent(event) end return end - local curH = theUnit:getLife() - local maxH = theUnit:getLife0() + if not theUnit.getName then return end -- WTF??? local uName = theUnit:getName() - if cfxSSBClient.verbose then - trigger.action.outText("+++SSB: Player leaves unit <" .. uName .. ">", 30) - trigger.action.outText("+++SSB: unit health check: " .. curH .. " of " .. maxH, 30) - end - cfxSSBClient.occupiedUnits[uName] = nil -- forget I was occupied cfxSSBClient.setSlotAccessForUnit(theUnit) -- prevent re-slotting if airfield lost return @@ -317,9 +332,10 @@ function cfxSSBClient:onEvent(event) end -- write down player names and planes - if event.id == 15 then -- birth + if event.id == 15 then -- birth / spawn if not event.initiator then return end local theUnit = event.initiator -- we know this exists + if not theUnit.getName then return end -- hardening local uName = theUnit:getName() if not uName then return end -- player entered unit? @@ -336,6 +352,12 @@ function cfxSSBClient:onEvent(event) end -- remember this unit as player controlled plane -- because player and plane can easily disconnect + -- find out if this is a dynamic spawn + local isDynamic = cfxMX.isDynamicPlayer(theUnit) + if isDynamic then + trigger.action.outText("+++SSBC: detected dynamic player spawn for unit <" .. uName .. ">, id <" .. theUnit:getID() .. ">, group <" .. theUnit:getGroup():getName() .. ">, player <" .. playerName .. ">", 30) + cfxSSBClient.amendPlayerData(theUnit) -- get airport and add to managed slots + end cfxSSBClient.playerPlanes[uName] = playerName if cfxSSBClient.verbose then trigger.action.outText("+++SSBC:SU: noted " .. playerName .. " piloting player unit " .. uName, 30) @@ -349,16 +371,21 @@ function cfxSSBClient:onEvent(event) if event.id == 5 then -- crash PRE-processing if not event.initiator then return end local theUnit = event.initiator + if not theUnit.getName then return end local uName = theUnit:getName() cfxSSBClient.occupiedUnits[uName] = nil -- no longer occupied cfxSSBClient.setSlotAccessForUnit(theUnit) -- prevent re-slotting if airfield lost + -- DO NOT RETURN NOW!!! singleuse proccing follows end if cfxSSBClient.singleUse and event.id == 5 then -- crash --if not event.initiator then return end local theUnit = event.initiator + if not theUnit then return end + if not theUnit.getName then return end local uName = theUnit:getName() if not uName then return end + if not theUnit.getGroup then return end local theGroup = theUnit:getGroup() if not theGroup then return end -- see if a player plane @@ -371,6 +398,7 @@ function cfxSSBClient:onEvent(event) return end -- if we get here, a player-owned plane has crashed + if not theGroup.getName then return end -- better safe than sorry local gName = theGroup:getName() if not gName then return end @@ -435,27 +463,34 @@ end -- pre-process static player data to minimize -- processor load on checks -function cfxSSBClient.processPlayerData() - cfxSSBClient.playerGroups = cfxMX.getPlayerGroup() - local pGroups = cfxSSBClient.playerGroups +function cfxSSBClient.processSSBPlayerData() + --cfxSSBClient.playerGroups = cfxMX.getPlayerGroup() + local pGroups = cfxSSBClient.SSBPlayerData -- cfxSSBClient.playerGroups local filteredPlayers = {} - for idx, theGroup in pairs(pGroups) do + for gName, theGroup in pairs(pGroups) do if theGroup.airfield ~= nil or cfxSSBClient.keepInAirGroups or cfxSSBClient.singleUse then -- only transfer groups that have airfields (or also keepInAirGroups or when single-use) -- attached. Ignore the rest as they are -- always fine - table.insert(filteredPlayers, theGroup) + --table.insert(filteredPlayers, theGroup) + filteredPlayers[gName] = theGroup end end cfxSSBClient.playerGroups = filteredPlayers + -- we can now relinquish SSBPlayerData + cfxSSBClient.SSBPlayerData = nil end -- add airfield information to each player group +-- WARNING: AMENDS/MODIFIES DATA IN MX TO CONTAIN AIRFIELDS +-- now changed to internal clones function cfxSSBClient.processGroupData() local pGroups = cfxMX.getPlayerGroup() -- we want the group.name attribute + local processed = {} for idx, theGroup in pairs(pGroups) do -- we always use the first player's plane as referenced + local cGroup = dcsCommon.clone(theGroup) local playerData = theGroup.playerUnits[1] local theAirfield = nil local delta = -1 @@ -471,20 +506,52 @@ function cfxSSBClient.processGroupData() end if delta > cfxSSBClient.maxAirfieldRange then -- forget airfield - theAirfield = nil + theAirfield = nil if cfxSSBClient.verbose then trigger.action.outText("+++SSB: group: " .. theGroup.name .. " unlinked - too far from airfield" , 30) end +-- end + else + cGroup.airfield = theAirfield -- we update the clone + -- add to my player groups, indexed by group name + processed[theGroup.name] = cGroup -- we keep the clone end - theGroup.airfield = theAirfield else if cfxSSBClient.verbose then trigger.action.outText("+++SSB: group: " .. theGroup.name .. " start option " .. action .. " does not concern SSB", 30) end end end + cfxSSBClient.SSBPlayerData = processed -- remember all relevant clones for post-processing end +function cfxSSBClient.amendPlayerData(theUnit) + -- enter with single, dynamically spawning unit and add an + -- entry for cfxSSBClient.SSBPlayerData to allow entry for + -- group's airbase / FARP + local dynGroup = theUnit:getGroup() + local theGroup = {} -- entry into db + theGroup.name = dynGroup:getName() + local thePoint = theUnit:getPoint() + local theAirfield, delta = cfxSSBClient.getClosestAirbaseTo(thePoint) + local afName = theAirfield:getName() + if cfxSSBClient.verbose then + trigger.action.outText("+++SSB: DYNAMIC group: " .. theGroup.name .. " closest to AF " .. afName .. ": " .. math.floor(delta) .. "m" , 30) + end + if delta > cfxSSBClient.maxAirfieldRange then + -- forget airfield + --theAirfield = nil + if cfxSSBClient.verbose then + trigger.action.outText("+++SSB: DYNAMIC group: " .. theGroup.name .. " unlinked - too far from airfield (???)" , 30 ) + end + else + if cfxSSBClient.verbose then + trigger.action.outText("+++SSB: DYNAMIC group: " .. theGroup.name .. " added to SSBPlayerData for slot management" , 30) + end + theGroup.uName = theUnit:getName() + cfxSSBClient.playerGroups[theGroup.name] = theGroup + end +end -- -- read config zone -- @@ -589,7 +656,7 @@ function cfxSSBClient.start() -- process player data to minimize effort and build cache -- into cfxSSBClient.playerGroups - cfxSSBClient.processPlayerData() + cfxSSBClient.processSSBPlayerData() -- processPlayerData() -- process ssbc zones -- for in-mission DML interface diff --git a/modules/TDZ.lua b/modules/TDZ.lua index cc50639..0d9c5fb 100644 --- a/modules/TDZ.lua +++ b/modules/TDZ.lua @@ -1,5 +1,5 @@ tdz = {} -tdz.version = "1.0.3" +tdz.version = "1.1.0" tdz.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -18,6 +18,7 @@ VERSION HISTORY 1.0.2 - manual placement option filters FARPs 1.0.3 - "manual" now defaults to false + 1.1.0 - now supports event 55 (runway touch) --]]-- @@ -326,7 +327,7 @@ function tdz:onEvent(event) if not theUnit.getPlayerName then return end local playerName = theUnit:getPlayerName() if not playerName then return end - if event.id == 4 then + if event.id == 4 or event.id == 55 then -- player landed tdz.playerLanded(theUnit, playerName) end diff --git a/modules/airfield.lua b/modules/airfield.lua index 44c35f4..832a1d3 100644 --- a/modules/airfield.lua +++ b/modules/airfield.lua @@ -1,5 +1,5 @@ airfield = {} -airfield.version = "2.1.1" +airfield.version = "2.2.0" airfield.requiredLibs = { "dcsCommon", "cfxZones", @@ -21,7 +21,7 @@ airfield.allAirfields = {} -- inexed by af name, db entries: base, cat -- support for FARPS as well 2.1.0 - added support for makeNeutral? 2.1.1 - bug fixing for DCS 2.9x airfield retrofit - + 2.2.0 - dmlZone:getCoalition() / masterowner adaptation for owner --]]-- -- init all airfields DB @@ -109,11 +109,6 @@ function airfield.createAirFieldFromZone(theZone) airfield.assumeControl(theZone) end - if theZone:hasProperty("ownedBy#") then - theZone.ownedBy = theZone:getStringFromZoneProperty("ownedBy#", "") - trigger.action.setUserFlag(theZone.ownedBy, theZone.owner) - end - -- if fixed attribute, we switch to that color and keep it fixed. -- can be overridden by either makeXX or autoCap. if theZone:hasProperty("fixed") then @@ -124,6 +119,11 @@ function airfield.createAirFieldFromZone(theZone) theZone.owner = theFixed end + if theZone:hasProperty("ownedBy#") then + theZone.ownedBy = theZone:getStringFromZoneProperty("ownedBy#", "") + trigger.action.setUserFlag(theZone.ownedBy, theZone.owner) + end + -- index by name, and warn if duplicate associate if airfield.myAirfields[theZone.afName] then trigger.action.outText("+++airF: WARNING - zone <" .. theZone.name .. "> redefines airfield <" .. theZone.afName .. ">, discarded!", 30) @@ -182,7 +182,7 @@ function airfield.showAirfield(theZone) local lineColor = theZone.redLine -- {1.0, 0, 0, 1.0} -- red local fillColor = theZone.redFill -- {1.0, 0, 0, 0.2} -- red - local owner = theZone.owner + local owner = theZone:getCoalition() -- .owner if owner == 2 then lineColor = theZone.blueLine -- {0.0, 0, 1.0, 1.0} fillColor = theZone.blueFill -- {0.0, 0, 1.0, 0.2} @@ -315,6 +315,7 @@ function airfield.update() if theZone.owner ~= 1 then -- only send cap event when capped airfield.airfieldCaptured(theAirfield) end + theZone.owner = 1 end if theZone.makeBlue and theZone:testZoneFlag(theZone.makeBlue, theZone.triggerMethod, "lastMakeBlue") then @@ -329,6 +330,7 @@ function airfield.update() if theZone.owner ~= 2 then -- only send cap event when capped airfield.airfieldCaptured(theAirfield) end + theZone.owner = 2 end if theZone.makeNeutral and theZone:testZoneFlag(theZone.makeNeutral, theZone.triggerMethod, "lastMakeNeutral") then @@ -343,6 +345,7 @@ function airfield.update() 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 + theZone.owner = 0 end diff --git a/modules/bombRange.lua b/modules/bombRange.lua index 7faacd8..fb6c9b1 100644 --- a/modules/bombRange.lua +++ b/modules/bombRange.lua @@ -1,5 +1,5 @@ bombRange = {} -bombRange.version = "2.0.0" +bombRange.version = "2.0.2" bombRange.dh = 1 -- meters above ground level burst bombRange.requiredLibs = { @@ -13,7 +13,7 @@ VERSION HISTORY *after* impact on high-resolution scans (30fps) set resolution to 30 ups by default order of events: check kills against dropping projectiles - collecd dead, and compare against missing erdnance while they are fresh + collect dead, and compare against missing erdnance while they are fresh GC interpolate hits on dead when looking at kills and projectile does not exist @@ -26,6 +26,9 @@ VERSION HISTORY 2.0.0 - support for radioMainMenu - support for types - types can have wild cards +2.0.1 - says hi! on start + - fixes for DCS Jul 11 bugs +2.0.2 - fixes for DCS Jul 22 bugs --]]-- @@ -197,7 +200,7 @@ function bombRange.initCommsForUnit(theUnit) if bombRange.mainMenu then mainMenu = radioMenu.getMainMenuFor(bombRange.mainMenu) -- nilling both next params will return menus[0] end - + if not theUnit.getName then return end -- Jul-22 DCS bug local uName = theUnit:getName() local pName = theUnit:getPlayerName() local theGroup = theUnit:getGroup() @@ -274,6 +277,7 @@ end -- Event Proccing -- function bombRange.suspectedHit(weapon, target) + if not Object.isExist(weapon) then return end -- DCS july 11 2024 issue local wType = weapon:getTypeName() if not target then return end if target:getCategory() == 5 then -- scenery @@ -407,15 +411,19 @@ end function bombRange:onEvent(event) if not event.initiator then return end local theUnit = event.initiator + if not Unit.isExist(theUnit) then return end -- DCS issue Jul-11 + if not theUnit.getName then return end -- DCS issue Jul-22 if event.id == 2 then -- hit: weapon still exists if not event.weapon then return end + if not Object.isExist(event.weapon) then return end -- Jul-11 issue bombRange.suspectedHit(event.weapon, event.target) return end if event.id == 28 then -- kill: similar to hit, but due to new mechanics not reliable if not event.weapon then return end + if not Object.isExist(event.weapon) then return end -- Jul-11 issue bombRange.suspectedHit(event.weapon, event.target) return end @@ -425,6 +433,7 @@ function bombRange:onEvent(event) -- these events can come *before* weapon disappears local killDat = {} killDat.victim = event.initiator + if not Object.isExist(event.initiator) then return end -- Jul-11 issue killDat.p = event.initiator:getPoint() killDat.when = timer.getTime() killDat.name = dcsCommon.uuid("vic") @@ -454,7 +463,8 @@ function bombRange:onEvent(event) end local w = event.weapon local b = {} - local bName = w:getName() + local bName = "unknown" + if w.getName then bName = w:getName() end -- Jul-22 DCS bug b.name = bName b.type = w:getTypeName() -- may need to verify type: how do we handle clusters or flares? @@ -793,6 +803,8 @@ function bombRange.start() -- start GC bombRange.GC() + -- say hi! + trigger.action.outText("cf/x Bomb Range v" .. bombRange.version .. " started.", 30) return true end diff --git a/modules/camp.lua b/modules/camp.lua index f079059..325e831 100644 --- a/modules/camp.lua +++ b/modules/camp.lua @@ -1,6 +1,6 @@ camp = {} camp.ups = 1 -camp.version = "1.0.2" +camp.version = "1.1.0" camp.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -18,6 +18,7 @@ VERSION HISTORY - actionSound - output sound with communications 1.0.2 - integration with FARPZones + 1.1.0 - support for DCS 2.9.6 dynamic spawns --]]-- -- -- CURRENTLY REQUIRES SINGLE-UNIT PLAYER GROUPS @@ -110,24 +111,55 @@ function camp.update() end function camp:onEvent(theEvent) - + if not theEvent then return end + if not theEvent.initiator then return end + local theUnit = theEvent.initiator +-- if not theUnit.getName then return end +-- if not theUnit.getPlayerName then return end + if not cfxMX.isDynamicPlayer(theUnit) then return end + local id = theEvent.id + if id == 15 then -- birth + camp.lateProcessPlayer(theUnit) + if camp.verbose then + trigger.action.outText("camp: late player processing for <" .. theUnit:getName() .. ">", 30) + end + end end -- -- Comms -- +function camp.lateProcessPlayer(theUnit) + if not theUnit then return end + if not theUnit.getGroup then return end + local theGroup = theUnit:getGroup() + local gName = theGroup:getName() + local gID = theGroup:getID() + camp.installComsFor(gID, gName) +end + +function camp.installComsFor(gID, gName) + local theRoot = missionCommands.addSubMenuForGroup(gID, "Funds / Repairs / Upgrades") + camp.roots[gName] = theRoot + local c00 = missionCommands.addCommandForGroup(gID, "Theatre Overview", theRoot, camp.redirectTFunds, {gName, gID, "tfunds"}) + local c0 = missionCommands.addCommandForGroup(gID, "Local Funds & Status Overview", theRoot, camp.redirectFunds, {gName, gID, "funds"}) + local c1 = missionCommands.addCommandForGroup(gID, "REPAIRS: Purchase local repairs", theRoot, camp.redirectRepairs, {gName, gID, "repair"}) + local c2 = missionCommands.addCommandForGroup(gID, "UPGRADE: Purchase local upgrades", theRoot, camp.redirectUpgrades, {gName, gID, "upgrade"}) +end + function camp.processPlayers() -- install coms stump for all players. they will be switched in/out -- whenever it is apropriate for idx, gData in pairs(cfxMX.playerGroupByName) do gID = gData.groupId gName = gData.name - local theRoot = missionCommands.addSubMenuForGroup(gID, "Funds / Repairs / Upgrades") +--[[-- local theRoot = missionCommands.addSubMenuForGroup(gID, "Funds / Repairs / Upgrades") camp.roots[gName] = theRoot local c00 = missionCommands.addCommandForGroup(gID, "Theatre Overview", theRoot, camp.redirectTFunds, {gName, gID, "tfunds"}) local c0 = missionCommands.addCommandForGroup(gID, "Local Funds & Status Overview", theRoot, camp.redirectFunds, {gName, gID, "funds"}) local c1 = missionCommands.addCommandForGroup(gID, "REPAIRS: Purchase local repairs", theRoot, camp.redirectRepairs, {gName, gID, "repair"}) - local c2 = missionCommands.addCommandForGroup(gID, "UPGRADE: Purchase local upgrades", theRoot, camp.redirectUpgrades, {gName, gID, "upgrade"}) + local c2 = missionCommands.addCommandForGroup(gID, "UPGRADE: Purchase local upgrades", theRoot, camp.redirectUpgrades, {gName, gID, "upgrade"}) --]]-- + camp.installComsFor(gID, gName) end end diff --git a/modules/cfxMX.lua b/modules/cfxMX.lua index 1d19b20..6304830 100644 --- a/modules/cfxMX.lua +++ b/modules/cfxMX.lua @@ -1,5 +1,5 @@ cfxMX = {} -cfxMX.version = "2.0.2" +cfxMX.version = "2.1.0" cfxMX.verbose = false --[[-- Mission data decoder. Access to ME-built mission structures @@ -13,7 +13,12 @@ cfxMX.verbose = false - harmonized with cfxGroups 2.0.1 - groupHotByName 2.0.2 - partOfGroupDataInZone(), allGroupsInZoneByData() from milHelo - + 2.0.3 - allGroupsInZoneByData supports type filtering + 2.1.0 - support for dynamically spawning player unit detection + - new isDynamicPlayer() + - new isMEPlayer() + - new isMEPlayerGroup() + --]]-- cfxMX.groupNamesByID = {} cfxMX.groupIDbyName = {} @@ -37,16 +42,14 @@ cfxMX.playerUnit2Group = {} -- returns a group data for player units. cfxMX.groups = {} -- all groups indexed b yname, cfxGroups folded into cfxMX --[[-- group objects are - { - name= "", - coalition = "" (red, blue, neutral), - coanum = # (0, 1, 2 for neutral, red, blue) - category = "" (helicopter, ship, plane, vehicle, static), - hasPlayer = true/false, - playerUnits = {} (for each player unit in group: name, point, action) - - } - +{ + name= "", + coalition = "" (red, blue, neutral), + coanum = # (0, 1, 2 for neutral, red, blue) + category = "" (helicopter, ship, plane, vehicle, static), + hasPlayer = true/false, + playerUnits = {} (for each player unit in group: name, point, action) +} --]]-- function cfxMX.getGroupFromDCSbyName(aName, fetchOriginal) if not fetchOriginal then fetchOriginal = false end @@ -359,11 +362,14 @@ function cfxMX.partOfGroupDataInZone(theZone, theUnits) -- move to mx? return false end -function cfxMX.allGroupsInZoneByData(theZone) -- returns groups indexed by name and count +function cfxMX.allGroupsInZoneByData(theZone, cat) -- returns groups indexed by name and count + if not cat then cat = {"helicopter", "ship", "plane", "vehicle" } end + if type(cat) == "string" then cat = {cat} end local theGroupsInZone = {} local count = 0 for groupName, groupData in pairs(cfxMX.groupDataByName) do - if groupData.units then + local gType = cfxMX.groupTypeByName[groupName] + if dcsCommon.arrayContainsString(cat, gType) and groupData.units then if cfxMX.partOfGroupDataInZone(theZone, groupData.units) then theGroupsInZone[groupName] = groupData -- DATA! work on clones! count = count + 1 @@ -375,7 +381,36 @@ function cfxMX.allGroupsInZoneByData(theZone) -- returns groups indexed by name end return theGroupsInZone, count end - + +function cfxMX.isDynamicPlayer(theUnit) + if not theUnit then return false end + if not theUnit.getName then return false end + if not theUnit.getPlayerName then return false end + if not theUnit:getPlayerName() then return false end + local uName = theUnit:getName() + if cfxMX.playerUnitByName[uName] then return false end + return true +end + +function cfxMX.isMEPlayer(theUnit) + if not theUnit then return false end + if not theUnit.getName then return false end + if not theUnit.getPlayerName then return false end + if not theUnit:getPlayerName() then return false end + local uName = theUnit:getName() + if cfxMX.playerUnitByName[uName] then return true end + return false +end + +function cfxMX.isMEPlayerGroup(theUnit) + if not theUnit then return false end + if not theUnit.getName then return end + if not theUnit.getPlayerName then return end + local uName = theUnit:getName() + if cfxMX.playerUnitByName[uName] then return true end + return false +end + function cfxMX.start() cfxMX.createCrossReferences() if cfxMX.verbose then diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index feaadb6..20643b0 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "4.3.6" +cfxZones.version = "4.4.2" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -9,38 +9,6 @@ cfxZones.version = "4.3.6" -- --[[-- VERSION HISTORY -- 4.0.0 - dmlZone OOP API started - - code revision / refactoring - - moved createPoint and copxPoint to dcsCommon, added bridging code - - re-routed all createPoint() invocations to dcsCommon - - removed anyPlayerInZone() because of cfxPlayer dependency - - numberArrayFromString() moved to dcsCommon, bridged - - flagArrayFromString() moved to dcsCommon, bridged - - doPollFlag() can differentiate between number method and string method - to enable passing an immediate negative value - - getNumberFromZoneProperty() enforces number return even on default - - immediate method switched to preceeding '#', to resolve conflict witzh - negative numbers, backwards compatibility with old (dysfunctional) method -- 4.0.1 - dmlZone:getName() -- 4.0.2 - removed verbosity from declutterZone (both versions) -- 4.0.3 - new processDynamicVZU() - - wildcard uses processDynamicVZU -- 4.0.4 - setFlagValue now supports multiple flags (OOP and classic) - - doSetFlagValue optimizations -- 4.0.5 - dynamicAB wildcard - - processDynamicValueVU -- 4.0.6 - hash mark forgotten QoL -- 4.0.7 - drawZone() -- 4.0.8 - markZoneWithObjects() - - cleanup - - markCenterWithObject - - markPointWithObject -- 4.0.9 - createPolyZone now correctly returns new zone - - createSimplePolyZone correctly passes location to createPolyZone - - createPolyZone now correctly sets zone.point - - createPolyZone now correctly inits dcsOrigin - - createCircleZone noew correctly inits dcsOrigin -- 4.0.10 - getBoolFromZoneProperty also supports "on" (=true) and "off" (=false) - 4.1.0 - getBoolFromZoneProperty 'on/off' support for dml variant as well - 4.1.1 - evalRemainder() updates - 4.1.2 - hash property missing warning @@ -49,7 +17,7 @@ cfxZones.version = "4.3.6" - small optimization for randomInRange() - randomDelayFromPositiveRange also allows 0 - 4.3.1 - new drawText() for zones - - dmlZones:getClosestZone() bridge + - dmlZone:getClosestZone() bridge - 4.3.2 - new getListFromZoneProperty() - 4.3.3 - hardened calculateZoneBounds - 4.3.4 - rewrote zone bounds for poly zones @@ -57,7 +25,13 @@ cfxZones.version = "4.3.6" - 4.3.6 - tiny optimization in isPointInsideQuad - moving zone - hardening code for static objects - moving zones - now deriving dx, dy,uHeading from dcsCommon xref for linked zones - +- 4.3.7 - corrected bug in processDynamicValues for lookup table +- 4.4.0 - dmlZone:getCoalition() + - dmlZone:getTypeName() + - dmlZone supports masterOwner by default + - dmlZone:getCoalition() dereferences masterOwner once +-4.4.1 - better verbosity for error in doPollFlag() +-4.4.2 - twn support for wildcards and --]]-- -- @@ -81,6 +55,13 @@ function dmlZone:new(o) return o end +-- dmlZone compatibility with DCS MSE objects: +-- dmlZone:getName() -- returns zone.name attribute (from ME) +-- dmlZone:getPoint() -- returns current point or dmlPoint +-- dmlZone:getTypeName() -- returns "dmlZone" +-- dmlZone:getCoalition -- returns owner + + -- -- CLASSIC INTERFACE -- @@ -1615,7 +1596,7 @@ function cfxZones.doPollFlag(theFlag, method, theZone) -- no OOP equivalent else if method ~= "on" and method ~= "f=1" then - trigger.action.outText("+++zones: unknown method <" .. method .. "> - using 'on'", 30) + trigger.action.outText("+++zones: unknown method <" .. method .. "> for flag <" .. theFlag .. "> in zone <" .. theZone.name .. "> - setting to 1", 30) end -- default: on. -- trigger.action.setUserFlag(theFlag, 1) @@ -2922,7 +2903,7 @@ function cfxZones.processDynamicValues(inMsg, theZone, msgResponses) -- access flag local val = cfxZones.getFlagValue(param, theZone) if not val or (val < 1) then val = 1 end - if val > msgResponses then val = msgResponses end + if val > #msgResponses then val = #msgResponses end val = msgResponses[val] val = dcsCommon.trim(val) @@ -2968,7 +2949,7 @@ end -- process function cfxZones.processDynamicLoc(inMsg, imperialUnits, responses) - local locales = {"lat", "lon", "ele", "mgrs", "lle", "latlon", "alt", "vel", "hdg", "rhdg", "type", "player"} + local locales = {"lat", "lon", "ele", "mgrs", "lle", "latlon", "alt", "vel", "hdg", "rhdg", "type", "player", "twn", "loc"} local outMsg = inMsg local uHead = 0 for idx, aLocale in pairs(locales) do @@ -3053,6 +3034,23 @@ function cfxZones.processDynamicLoc(inMsg, imperialUnits, responses) elseif aLocale == "rhdg" and (responses) then local offset = cfxZones.rspMapper360(uHead, #responses) locString = dcsCommon.trim(responses[offset]) + elseif aLocale == "twn" then + if twn and towns then locString = twn.closestTownTo(thePoint) + else locString = "!twn!" end + elseif aLocale == "loc" then + if twn and towns then + local name, data, dist = twn.closestTownTo(thePoint) + local units = "km" + local mdist= dist * 0.539957 + dist = math.floor(dist/100) / 10 + mdist = math.floor(mdist/100) / 10 + if imperialUnits then + dist = mdist + units = "nm" + end + local bear = dcsCommon.compassPositionOfARelativeToB(thePoint, data.p) + locString = dist .. units .. " " .. bear .. " of " .. name + else locString = "!twn!" end else -- we have mgrs local grid = coord.LLtoMGRS(coord.LOtoLL(thePoint)) @@ -3345,6 +3343,24 @@ function dmlZone:getName() -- no cfxZones.bridge! return self.name end +function dmlZone:getCoalition() + -- automatically support masterOwner. Warning: cloners etc can reference itself! + if self.masterOwner then return self.masterOwner.owner end -- zone must exist + return self.owner +end + +function cfxZones.getCoalition(theZone) + return theZone:getCoalition() +end + +function dmlZone:getTypeName() + return "dmlZone" +end + +function cfxZones.getTypeName(theZone) + return theZone:getTypeName() +end + function cfxZones.linkUnitToZone(theUnit, theZone, dx, dy) -- note: dy is really Z, don't get confused!!!! theZone.linkedUnit = theUnit if not dx then dx = 0 end @@ -3693,6 +3709,20 @@ function cfxZones.init() -- much like verbose, all zones have owner for n, aZone in pairs(cfxZones.zones) do aZone.owner = cfxZones.getCoalitionFromZoneProperty(aZone, "owner", 0) + + if aZone:hasProperty("masterOwner") then + local mo = aZone:getStringFromZoneProperty("masterOwner", "forgotten master") + mo = dcsCommon.trim(mo) + if mo == "*" then mo = aZone.name end + local mz = cfxZones.getZoneByName(mo) + if not mz then + trigger.action.outText("+++fcxZones: WARNING: Master Owner <" .. mo .. "> for zone <" .. aZone.name .. "> does not exist!", 30) + else + aZone.masterOwner = mz + aZone.owner = mz.owner + end + end + end -- enable all zone's verbose flags if present diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index a6148f7..ef03d40 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -1,5 +1,5 @@ cloneZones = {} -cloneZones.version = "2.3.0" +cloneZones.version = "2.4.0" cloneZones.verbose = false cloneZones.requiredLibs = { "dcsCommon", -- always @@ -56,6 +56,7 @@ cloneZones.respawnOnGroupID = true (undocumented, just to provide lazy people with a migration path) with wiper module - using "wipe?" will now create a warning + 2.4.0 - reworked masterOwner to fit with dmlZone --]]-- -- @@ -268,6 +269,7 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" theZone.cloneMethod = theZone:getStringFromZoneProperty("method", "inc") -- note string on number default end + --[[-- if theZone:hasProperty("masterOwner") then theZone.masterOwner = theZone:getStringFromZoneProperty( "masterOwner", "*") theZone.masterOwner = dcsCommon.trim(theZone.masterOwner) @@ -285,7 +287,9 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" if not theMaster then trigger.action.outText("clnZ: WARNING: cloner's <" .. theZone.name .. "> master owner named <" .. theZone.masterOwner .. "> does not exist!", 30) end + theZone.masterOwner = theMaster end + --]]-- theZone.turn = theZone:getNumberFromZoneProperty("turn", 0) @@ -692,7 +696,8 @@ function cloneZones.sameIDUnitData(theData) end function cloneZones.resolveOwnership(spawnZone, ctry) - if not spawnZone.masterOwner then return ctry end + if not spawnZone.masterOwner then return ctry end -- old code +--[[-- local masterZone = cfxZones.getZoneByName(spawnZone.masterOwner) if not masterZone then trigger.action.outText("+++clnZ: cloner " .. spawnZone.name .. " could not find master owner <" .. spawnZone.masterOwner .. ">", 30) @@ -702,8 +707,10 @@ function cloneZones.resolveOwnership(spawnZone, ctry) if not masterZone.owner then return ctry end +--]]-- - ctry = dcsCommon.getACountryForCoalition(masterZone.owner) +-- ctry = dcsCommon.getACountryForCoalition(masterZone.owner) + ctry = dcsCommon.getACountryForCoalition(spawnZone:getCoalition()) return ctry end @@ -1694,6 +1701,8 @@ function cloneZones.hasLiveUnits(theZone) end function cloneZones.resolveOwningCoalition(theZone) + return theZone:getCoalition() +--[[-- if not theZone.masterOwner then return theZone.owner end local masterZone = cfxZones.getZoneByName(theZone.masterOwner) if not masterZone then @@ -1701,6 +1710,7 @@ function cloneZones.resolveOwningCoalition(theZone) return theZone.owner end return masterZone.owner +--]]-- end function cloneZones.getRequestableClonersInRange(aPoint, aRange, aSide) diff --git a/modules/convoy.lua b/modules/convoy.lua index 3fd9a7c..70454ba 100644 --- a/modules/convoy.lua +++ b/modules/convoy.lua @@ -1,112 +1,436 @@ convoy = {} -convoy.version = "0.0.0" +convoy.version = "1.1.0" convoy.requiredLibs = { "dcsCommon", "cfxZones", "cfxMX", } convoy.zones = {} -convoy.running = {} +convoy.convoys = {} -- running convoys convoy.ups = 1 +convoy.convoyWPReached = {} +convoy.convoyAttacked = {} +convoy.convoyDestroyed = {} +convoy.convoyArrived = {} +convoy.roots = {} -- group comms +convoy.uuidNum = 1 + +--[[-- +A DML module (C) 2024 by Christian Franz + +VERSION HISTORY +1.0.0 - Initial version +1.1.0 - MUCH better reporting for all coalitions + - remaining unit count on successful attack in reports + - warning when arriving at penultimate + - corrected method for polling when dead etc. + - anon name for reporting + - anon uuid + - actionSound + - support for attachTo: + - warning when not onStart and no start? + - say hi + - removed destination attribute + +--]]-- + +--[[-- CONVOY Structure + .name (uuid) name of convoy + .dest destination string, defaulted from theZone.destinations + .destObject destination object from theZone.destobject + .waypoints array of vec3 points, one for each wp + .currWP -- index of last WP reached, inited to 1 on start + .origin zone where convoy spawned + .groups array groups (only one element) that this convoy consists of, ground only + .helos array if defined, helicopters escorting.0 One group only + .groupSizes dict by groupname of group size to detect loss, updated + .lastAttackReport in time seconds since last report to enable pauses between reports + .coa coalition this convay belongs to + .reached indexed by waypoint. true if we reached that wp. + .distance = total length of route + .oName = original name of gorup + .desc = name, from, to as text + .wasAttacked true after first successful attack, for remain count +--]]-- + +-- +-- Misc +-- +function convoy.uuid() + convoy.uuidNum = convoy.uuidNum + 1 + return convoy.uuidNum +end +-- +-- callbacks +-- +function convoy.installWPCallback(theCB) + table.insert(convoy.convoyWPReached, theCB) +end + +function convoy.invokeWPCallbacks(theConvoy, wp, wpnum) + for idx, cb in pairs(convoy.convoyWPReached) do + cb(theConvoy, wp, wpnum) + end +end + + +function convoy.installAttackCallback(theCB) + table.insert(convoy.convoyAttacked, theCB) +end + +function convoy.invokeAttackedCallbacks(theConvoy) + for idx, cb in pairs(convoy.convoyAttacked) do + cb(theConvoy) + end +end + + +function convoy.installDestroyed(theCB) + table.insert(convoy.convoyDestroyed, theCB) +end + +function convoy.invokeDestroyedCallbacks(theConvoy) + for idx, cb in pairs(convoy.convoyDestroyed) do + cb(theConvoy) + end +end + +function convoy.installArrived(theCB) + table.insert(convoy.convoyArrived, theCB) +end +function convoy.invokeArrivedCallbacks(theConvoy) + for idx, cb in pairs(convoy.convoyArrived) do + cb(theConvoy) + end +end +-- +-- Reading Zones +-- function convoy.addConvoyZone(theZone) convoy.zones[theZone.name] = theZone end +function convoy.thereCanOnlyBeOne(theDict) + for key, value in pairs (theDict) do + local ret = {} + ret[key] = value + return ret + end +end + + function convoy.readConvoyZone(theZone) theZone.coa = theZone:getCoalitionFromZoneProperty("coalition", 0) - if theZone:hasProperty("masterOwner") then - local mo = theZone:getStringFromZoneProperty("masterOwner") - local mz = cfxZones.getZoneByName(mo) - if not mz then - trigger.action.outText("+++cvoy: WARNING: Master Owner <" .. mo .. "> for zone <" .. theZone.name .. "> does not exist!", 30) - else - theZone.masterOwner = mz - end - theZone.isDynamic = theZone:getBoolFromZoneProperty("dynamic", true) - end + theZone.isDynamic = theZone:getBoolFromZoneProperty("dynamic", false) -- get groups inside me. - local myGroups, count = cfxMX.allGroupsInZoneByData(theZone) - trigger.action.outText("zone <" .. theZone.name .. ">: <" .. count .. "> convoy groups", 30) - theZone.myGroups = myGroups - theZone.unique = theZone:getBoolFromZoneProperty("unique", true) - theZone.preWipe = theZone:getBoolFromZoneProperty("preWipe", true) or theZone.unique - theZone.onStart = theZone:getBoolFromZoneProperty("onStart", false) + local myGroups, count = cfxMX.allGroupsInZoneByData(theZone, {"vehicle"}) + local myHelos = {} -- for spawning only. + local myHelos, hcount = cfxMX.allGroupsInZoneByData(theZone, {"helicopter"}) + + if count < 1 then + trigger.action.outText("cnvy: WARNING: convoy zone <" .. theZone.name .. "> has no vehicles.", 30) + end - -- wipe all existing + -- process destinations for each vehicle group + local destinations = {} + local destObjects = {} + local distances = {} + --local froms = {} + for gName, gData in pairs (myGroups) do + local dest, destObj = convoy.getDestinationForData(gData) + destinations[gName] = dest + destObjects[gName] = destObj -- nearest DCS/DML objects + distances[gName] = convoy.getDistanceForData(gData) + --froms[gName] = convoy.getSourceForData(theZone) + end + + theZone.myGroups = myGroups -- vehicles only. only one chosen per spawn + -- detination calc'd on-demand if nil + -- dict by name + theZone.destinations = destinations + theZone.destObjects = destObjects + theZone.distances = distances + theZone.froms = convoy.getSourceForData(theZone) + + theZone.myHelos = myHelos -- helos can only escort, don't count + -- helos wonky with multi-groups because dcs + -- linking to groups + theZone.identical = theZone:getBoolFromZoneProperty("identical", false) + theZone.unique = not theZone.identical + theZone.preWipe = theZone:getBoolFromZoneProperty("preWipe", false) or theZone.identical + theZone.endWipe = theZone:getBoolFromZoneProperty("endWipe", true) + theZone.killWipeDelay = theZone:getNumberFromZoneProperty("killWipeDelay", 300) -- leave helos as angry hornets for a while (5 Min = 300s) + + theZone.pEscort = theZone:getNumberFromZoneProperty("pEscort", 100) -- in percent (= * 100) + theZone.onStart = theZone:getBoolFromZoneProperty("onStart", false) + theZone.wpUpdates = theZone:getBoolFromZoneProperty("wpUpdates", true) + theZone.attackWarnings = theZone:getBoolFromZoneProperty("attackWarnings", true) + if theZone:hasProperty("spawn?") then + theZone.spawnFlag = theZone:getStringFromZoneProperty("spawn?", "none") + theZone.lastSpawnFlag = trigger.misc.getUserFlag(theZone.spawnFlag) + else + if not theZone.onStart then + 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 + --[[-- + if theZone:hasProperty("destination") then -- remove me + theZone.destination = theZone:getStringFromZoneProperty("destination", "none") + end + --]]-- + if theZone:hasProperty("dead!") then + theZone.deadOut = theZone:getStringFromZoneProperty("dead!", "none") + end + if theZone:hasProperty("attacked!") then + theZone.attackedOut = theZone:getStringFromZoneProperty("attacked!", "none") + end + if theZone:hasProperty("arrived!") then + theZone.arrivedOut = theZone:getStringFromZoneProperty("arrived!", "none") + end + -- wipe all existing vehicle and helos for groupName, data in pairs(myGroups) do local g = Group.getByName(groupName) if g then Group.destroy(g) end - end + end + for groupName, data in pairs(myHelos) do + local g = Group.getByName(groupName) + if g then + Group.destroy(g) + end + end end -function convoy.startConvoy(theZone) - -- make sure my coa is set up correctly - local mo = theZone.masterOwner - if mo then - theZone.owner = mo.owner - if theZone.isDynamic then - theZone.coa = mo.owner +function convoy.getDistanceForData(theData) + local total = 0 + if not theData then return 0 end + local route = theData.route + local points = route.points + local wpNum = #points + if wpNum < 2 then return 0 end + local i = 1 + local t = points[1] + local last = {x=t.x, y = 0, z = t.y} + while i < wpNum do + i = i + 1 + local t = points[i] + local now = {x=t.x, y = 0, z = t.y} + total = total + dcsCommon.dist(last, now) + last = now + end + return total +end + +function convoy.getLocName(p) -- returns a string and bool (success) + local msg = "" + local success = false + if twn and towns then + local name, data, dist = twn.closestTownTo(p) + local mdist= dist * 0.539957 + dist = math.floor(dist/100) / 10 + mdist = math.floor(mdist/100) / 10 + local bear = dcsCommon.compassPositionOfARelativeToB(p, data.p) + msg = dist .. "km/" .. mdist .."nm " .. bear .. " of " .. name + success = true + end + return msg, success +end + +function convoy.getSourceForData(theZone) + if twn and towns then + local currPoint = theZone:getPoint() + local name, data, dist = twn.closestTownTo(currPoint) + local mdist= dist * 0.539957 + dist = math.floor(dist/100) / 10 + mdist = math.floor(mdist/100) / 10 + local bear = dcsCommon.compassPositionOfARelativeToB(currPoint, data.p) + return dist .. "km/" .. mdist .. "nm " .. bear .. " of " .. name + end + return theZone.name +end + +function convoy.getDestinationForData(theData) + -- dest is filled with town data if available + -- destObject is nearest dmlZone or airfield, whatever closest + local dest = "unknown" + local destObj + local destType + if theData then + -- access route points + local route = theData.route + local points = route.points + local wpnum = #points + local lastWP = points[wpnum] + local thePoint = {x=lastWP.x, y=0, z=lastWP.y} -- !!! + local hasTwn = false + dest, hasTwn = convoy.getLocName(thePoint) + local clsZ, zDelta = cfxZones.getClosestZone(thePoint) + local clsA, aDelta = dcsCommon.getClosestAirbaseTo(thePoint, 0) + local dist = zDelta + destType = "dmlZone" + destObj = clsZ + if aDelta < dist then -- airfield is closer than closest zone + dist = aDelta + destType = "airfield" + destObj = clsA end + if convoy.verbose then + trigger.action.outText(theData.name .. " has destination object " .. destObj:getName(), 30) + end + if not hasTwn then + -- use nearest dmlZone or airfield to name destination + dest = destObj:getName() + if dist < 10000 then + elseif dist < 20000 then + dest = dest .. " area" + else + dest = "greater " .. dest .. " area" + end + end + else + dest = "NO GROUP DATA" + end + return dest, destObj, destType +end + +function convoy.startConvoy(theZone, groupIdent) + -- groupIdent overrides random selection and pickes exactly that group + -- make sure my coa is set up correctly + -- groupIdent is a string (group name) + if theZone.isDynamic then + theZone.coa = theZone:getCoalition() -- auto-resolves masterowner end - -- iterate all groups - local spawns = {} - for gName, gOrig in pairs(theZone.myGroups) do - trigger.action.outText("convoy: startting group <" .. gName .. "> for zone <" .. theZone.name .. ">", 30) - local gData = dcsCommon.clone(gOrig) - -- make unique names for group and units if desired + -- pre-wipe existing convoys if they exist, will NOT cause + -- failed event! + if theZone.preWipe then + local groupCollector = {} + local filtered = {} + for cName, entry in pairs(convoy.convoys) do + if entry.origin == theZone then + for gName, theGroup in pairs(entry.groups) do -- vehicles + if Group.isExist(theGroup) then + table.insert(groupCollector, theGroup) + end + end + for gName, theGroup in pairs(entry.helos) do -- helicopters + if Group.isExist(theGroup) then + table.insert(groupCollector, theGroup) + end + end + -- do not pass on + else + filtered[cName] = entry -- pass on + end + end + + -- delete the groups that are still alive + for idx, theGroup in pairs(groupCollector) do + trigger.action.outText("cnvy: prewipe - removing group <" .. theGroup:getName() .. ">", 30) + Group.destroy(theGroup) + end + convoy.convoys = filtered + end + + -- iterate all groups and spawn them + local spawns = {} -- vehicle groups -- DICT + local helos = {} + local spawnSizes = {} -- overkill, just one group in here + local theConvoy = {} -- data carrier + theConvoy.name = dcsCommon.uuid(theZone.name) + theConvoy.anon = "CVY-" .. convoy.uuid() + -- choose a vehicle group from all available + local gOrig + if groupIdent then gOrig = theZone.myGroups[groupIdent] + else + local allGroups = dcsCommon.enumerateTable(theZone.myGroups) + gOrig = dcsCommon.pickRandom(allGroups) + end + local gName = gOrig.name + local gData = dcsCommon.clone(gOrig) + -- make unique names for group and units if desired + if theZone.unique then + gData.name = dcsCommon.uuid(gOrig.name) + gData.groupId = nil + for idx, theUnit in pairs (gData.units) do + theUnit.name = dcsCommon.uuid(theUnit.name) + theUnit.unitId = nil + end + end + + -- retrieve destination from zone + local dest = theZone.destination + if not dest then + dest = theZone.destinations[gName] + end + local from = theZone.froms -- they are all the same... + theConvoy.dest = dest + theConvoy.destObject = theZone.destObjects[gName] + theConvoy.desc = theConvoy.name .. " from " .. from .. " to " .. dest + + -- spawn one vehicle group and add it to my spawns and spawnSizes + gCat = Group.Category.GROUND + local waypoints = convoy.amendVehicleData(theZone, gData, theConvoy.name) -- add actions to route, return waypoint locations + theConvoy.waypoints = waypoints + local cty = dcsCommon.getACountryForCoalition(theZone.coa) + local theSpawnedGroup = coalition.addGroup(cty, gCat, gData) + spawns[gData.name] = theSpawnedGroup + spawnSizes[gData.name] = theSpawnedGroup:getSize() + theConvoy.distance = theZone.distances[gOrig.name] + theConvoy.oName = gOrig.name + + -- now spawn one helo group and make them escort the group that was + -- just spawned + local rnd = math.random(1,100) + if (rnd <= theZone.pEscort) and dcsCommon.getSizeOfTable(theZone.myHelos) > 0 then + gCat = Group.Category.HELICOPTER -- allow escort helos + allGroups = dcsCommon.enumerateTable(theZone.myHelos) + gOrig = dcsCommon.pickRandom(allGroups) + gData = dcsCommon.clone(gOrig) + -- make group unique if theZone.unique then gData.name = dcsCommon.uuid(gOrig.name) + gData.groupId = nil for idx, theUnit in pairs (gData.units) do theUnit.name = dcsCommon.uuid(theUnit.name) + theUnit.unitId = nil end end - convoy.amendData(theZone, gData) -- add actions to route - -- wipe existing if requested - if theZone.preWipe then - - end - local catRaw = cfxMX.groupTypeByName[gName] - local gCat = Group.Category.GROUND - if catRaw == "helicopter" then - gCat = Group.Category.HELICOPTER - elseif catRaw == "plane" then - gCat = Group.Category.AIRPLANE - elseif catRaw == "vehicle" then - gCat = Group.Category.GROUND - else -- missing so far: ship - trigger.action.outText("+++milH: ignored group <" .. gName .. ">: unknown type <" .. catRaw .. ">", 30) - end - local cty = dcsCommon.getACountryForCoalition(theZone.coa) - local theSpawnedGroup = coalition.addGroup(cty, gCat, gData) - spawns[gData.name] = theSpawnedGroup - trigger.action.outText("convoy <" .. theSpawnedGroup:getName() .. "> spawned for <" .. theZone.name .. ">", 30) + convoy.makeHeloDataEscortGroup(gData, theSpawnedGroup) + local theSpawnedHelos = coalition.addGroup(cty, gCat, gData) + helos[gData.name] = theSpawnedHelos end + + theConvoy.origin = theZone + theConvoy.groups = spawns -- contains only one group + theConvoy.helos = helos -- all helos spawned + theConvoy.groupSizes = spawnSizes -- vehicle group size by name + theConvoy.lastAttackReport = -1000 + theConvoy.coa = theZone.coa + theConvoy.reached = {} -- waypoints reached message remember + -- add to convoys + convoy.convoys[theConvoy.name] = theConvoy + -- return the convoy entry + return theConvoy end -function convoy.amendData(theZone, theData) +function convoy.amendVehicleData(theZone, theData, convoyName) -- place a callback action for each waypoint -- in data block - if not theData.route then return end + if not theData.route then return nil end local route = theData.route - if not route.points then return end + if not route.points then return nil end local points = route.points local np = #points - if np < 1 then return end - trigger.action.outText("convoy: group <" .. theData.name .. ">, zone <" .. theZone.name .. ">, points=<" .. np .. ">", 30) + if np < 1 then return nil end --- for i=1, np do local newPoints = {} - for idx, aPoint in pairs(points) do - local wp = dcsCommon.clone(aPoint) -- points[i] + local waypoints = {} + for idx=1, np do + local wp = points[idx] local tasks = wp.task.params.tasks - --local i = idx --- if not tasks then tasks = {} end --- if tasks then --- dcsCommon.dumpVar2Str("RAW tasks 1bc " .. idx, tasks) local tnew = #tasks + 1 -- new number for this task local t = { ["number"] = tnew, @@ -117,54 +441,563 @@ function convoy.amendData(theZone, theData) ["action"] = { ["id"] = "Script", ["params"] = { - ["command"] = "trigger.action.outText(\"convoy reached WP Index " .. idx .." = WP(" .. idx-1 .. ") of " .. np .. "\", 30)", + ["command"] = "convoy.wpReached(\"" .. theData.name .."\", \"" .. convoyName .. "\", \"" .. idx .. "\", \"" .. np .. "\")", }, -- end of ["params"] }, -- end of ["action"] }, -- end of ["params"] } -- end of task -- add t to tasks table.insert(tasks, t) --- tasks[tnew] = t --- dcsCommon.dumpVar2Str("tasks for modded 1bc " .. idx, tasks) - newPoints[idx] = wp - trigger.action.outText("convoy: added wp task to wp <" .. idx .. ">", 30) - -- end --- dcsCommon.dumpVar2Str("modded point 1BC WP" .. idx, wp) + newPoints[idx] = wp + local thePoint = {x=wp.x, y=0, z=wp.y} + waypoints[idx] = thePoint end route.points = newPoints --- dcsCommon.dumpVar2Str("points", points) - + return waypoints end +function convoy.makeHeloDataEscortGroup(theData, theGroup) + -- overwrite entire route with new escort mission for theGroup + local gID = theGroup:getID() + -- set group's main task to CAS + theData.tasks = {} + theData.task = "CAS" + local nuPoints = {} + local oldPoints = theData.route.points + local wp1 = dcsCommon.clone(oldPoints[1]) -- clone old + wp1.alt = 100 -- overwrite key data + wp1.action = "Turning Point" + wp1.alt_type = "RADIO" + wp1.speed = 28 + wp1.task = { + ["id"] = "ComboTask", + ["params"] = { + ["tasks"] = { + [1] = { + ["enabled"] = true, + ["key"] = "CAS", + ["id"] = "EngageTargets", + ["number"] = 1, + ["auto"] = true, + ["params"] = { + ["targetTypes"] = + { + [1] = "Helicopters", + [2] = "Ground Units", + [3] = "Light armed ships", + }, -- end of ["targetTypes"] + ["priority"] = 0, + }, -- end of ["params"] + }, -- end of [1] + [2] = { + ["enabled"] = true, + ["auto"] = false, + ["id"] = "GroundEscort", + ["number"] = 2, + ["params"] = { + ["targetTypes"] = { + [1] = "Helicopters", + [2] = "Ground Units", + }, -- end of ["targetTypes"] + ["groupId"] = gID, -- ESCORT THIS! + ["lastWptIndex"] = 2, + ["engagementDistMax"] = 500, + ["lastWptIndexFlag"] = false, + ["lastWptIndexFlagChangedManually"] = false, + }, -- end of ["params"] + }, -- end of [2] + }, -- end of ["tasks"] + }, -- end of ["params"] + } -- end of ["task"] + wp1.type = "Turning Point" + nuPoints[1] = wp1 + theData.route.points = nuPoints +end + +-- +-- WP Callback +-- +function convoy.wpReached(gName, convName, idx, wpNum) + idx = tonumber(idx) + wpNum = tonumber(wpNum) + local theConvoy = convoy.convoys[convName] + if not theConvoy then + trigger.action.outText("convoy <" .. convName .. "> not found, exiting", 30) + return + end + local waypoints = theConvoy.waypoints + theConvoy.currWP = idx + local coa = theConvoy.coa + local enemy = 1 + if coa == 1 then enemy = 2 end + local theZone = theConvoy.origin + if theConvoy.reached[idx] then + trigger.action.outText("<" .. convName .. ">: We've been here before...?", 30) + else + convoy.invokeWPCallbacks(theConvoy, idx, wpNum) + theConvoy.reached[idx] = true -- remember we were reported this + if idx == 1 then + local distk = math.floor(theConvoy.distance / 1000 + 1.5) + local distm = math.floor(0.621371 * theConvoy.distance/1000 + 1) + + trigger.action.outTextForCoalition(coa, "Convoy " .. convName .. " has departed from rallying point " .. theZone.froms .. " towards their destination " .. theConvoy.dest .. " (for a total distance of " .. distk .. "km/" .. distm .. "nm).", 30) + trigger.action.outSoundForCoalition(coa, convoy.actionSound) + if convoy.listEnemy then + local msg = "Intelligence reports new enemy convoy " .. theConvoy.anon .. " enroute to " .. theConvoy.destObject:getName() + trigger.action.outTextForCoalition(enemy, msg, 30) + trigger.action.outSoundForCoalition(enemy, convoy.actionSound) + end + + elseif idx == wpNum then + trigger.action.outTextForCoalition(coa, "Convoy " .. convName .. " has arrived at desitation (" .. theConvoy.dest .. ").", 30) + trigger.action.outSoundForCoalition(coa, convoy.actionSound) + if convoy.listEnemy then + local msg = "Enemy convoy " .. theConvoy.anon .. " arrived at " .. theConvoy.destObject:getName() + trigger.action.outTextForCoalition(enemy, msg, 30) + trigger.action.outSoundForCoalition(enemy, convoy.actionSound) + end + convoy.invokeArrivedCallbacks(theConvoy) + -- hit the output flag if defined + if theZone.arrivedOut then + theZone:pollFlag(theZone.arrivedOut, "inc") + end + -- remove convoy from watchlist + convoy.convoys[convName] = nil + + -- deallocate convoy if theZone requests is + if theZone.endWipe then + convoy.wipeConvoy(theConvoy) + end + else + if theZone.wpUpdates then + local p = waypoints[idx] -- idx is one-based! + local msg = "Convoy " .. convName .. ", enroute to destination " .. theConvoy.dest .. ", has reached " + local locName, hasLoc = convoy.getLocName(p) + if hasLoc then + msg = msg .. "checkpoint located at " .. locName .. " (waypoint " .. idx .. " of " .. wpNum .. ")." + else + msg = msg .. "waypoint " ..idx .. " of " .. wpNum .. "." + end + trigger.action.outTextForCoalition(coa, msg, 30) + trigger.action.outSoundForCoalition(theConvoy.coa, convoy.actionSound) + end + end + end +end + +function convoy.wipeConvoy(theConvoy) -- called async and sync + local theZone = theConvoy.origin + if convoy.verbose or theZone.verbose then + trigger.action.outText("+++cnvy: entere wipe for convoy <" .. theConvoy.name .. "> started from <" .. theZone.name .. ">", 30) + end + for gName, theGroup in pairs(theConvoy.groups) do + if Group.isExist(theGroup) then + Group.destroy(theGroup) + end + end + for gName, theGroup in pairs(theConvoy.helos) do + if Group.isExist(theGroup) then + Group.destroy(theGroup) + end + end +end + +-- +-- API +-- + +function convoy.collectConvoysFor(coa) + local collector = {} + for idx, theZone in pairs(convoy.zones) do + if theZone.isDynamic then + theZone.coa = theZone:getCoalition() + end + -- warning: differentiating between coa and owner! + if theZone.coa == coa then + table.insert(collector, theZone) + end + end + return collector +end + +function convoy.sourceAndDestinationForCoa(theList, coa, allowNeutral) + local solutions = {} + for idx, theZone in pairs(theList) do + -- all destinations have a coalition + for gName, theObject in pairs(theZone.destObjects) do + -- destObjects can be dml zones or airbases + local oCoa = theObject:getCoalition() -- dmlZones return Owner and respect masterowner + if oCoa == coa or (allowNeutral and oCoa == 0) then + local aMatch = {theZone=theZone, gName=gName} + table.insert(solutions, aMatch) + end + end + end + return solutions +end + +function convoy.filterConvoysByDistance(theList, maxDist) + local filtered = {} + for idx, theEntry in pairs(theList) do + local theZone = theEntry.theZone + local gName = theEntry.gName + local cDist = theZone.distances[gName] + if cDist < maxDist then + table.insert(filtered, theEntry) + end + end + + return filtered +end + +function convoy.filterConvoysByRunning(theList) + -- filters all zones that have a running convoy + local filtered = {} + for idx, theZone in pairs(theList) do -- iterate all zones + local pass = true + local coa = theZone.coa -- not getCoalition! + for name, entry in pairs(convoy.convoys) do + if entry.coa == coa and entry.origin == theZone then + pass = false + end + end + if pass then + table.insert(filtered, theZone) + else + + end + end + return filtered +end + +function convoy.getSafeConvoyForCoa(coa, allowNeutral, maxDist) + local allMyConvoys = convoy.collectConvoysFor(coa) + local safeConvoys = convoy.sourceAndDestinationForCoa(allMyConvoys, coa, allowNeutral) + if convoy.verbose then + trigger.action.outText("+++safe convoy scan for <" .. coa .. "> returns <" .. #safeConvoys .. "> hits out of <" .. #allMyConvoys .. "> potentials:", 30) + for idx, theSol in pairs(safeConvoys) do + trigger.action.outText("zone <" .. theSol.theZone.name .. ">, group <" .. theSol.gName .. ">", 30) + end + end + + if maxDist then + safeConvoys = convoy.filterConvoysByDistance(safeConvoys, maxDist) + end + + if #safeConvoys < 1 then + return nil + end + local sol = dcsCommon.pickRandom(safeConvoys) + + return sol.theZone, sol.gName +end + +function convoy.runningForCoa(coa) + local count = 0 + for name, entry in pairs(convoy.convoys) do + if entry.coa == coa then count = count + 1 end + end + return count +end + +-- +-- Event & Comms +-- +function convoy:onEvent(theEvent) + if not theEvent then return end + if not theEvent.initiator then return end + local theUnit = theEvent.initiator + if not theUnit.getName then return end + if not theUnit.getGroup then return end + if not theUnit.getPlayerName then return end + if not theUnit:getPlayerName() then return end + local ID = theEvent.id + if ID == 15 then -- birth + convoy.installComms(theUnit) + end +end + +function convoy.installComms(theUnit) + if not convoy.hasGUI then return end + if not theUnit then return end + local theGroup = theUnit:getGroup() + local gID = theGroup:getID() + local gName = theGroup:getName() + + -- remove old group menu + if convoy.roots[gName] then + missionCommands.removeItemForGroup(gID, convoy.roots[gName]) + end + + -- handle main menu + local mainMenu = nil + if convoy.mainMenu then + mainMenu = radioMenu.getMainMenuFor(convoy.mainMenu) + end + + local root = missionCommands.addSubMenuForGroup(gID, convoy.menuName, mainMenu) + convoy.roots[gName] = root + args = {} + args.theUnit = theUnit + args.gID = gID + args.gName = gName + args.coa = theGroup:getCoalition() + -- now add the submenus for convoys + local m = missionCommands.addCommandForGroup(gID, "List known Convoys", root, convoy.redirectListConvoys, args) +end + +function convoy.redirectListConvoys(args) + timer.scheduleFunction(convoy.doListConvoys, args, timer.getTime() + 0.1) +end + +function convoy.doListConvoys(args) +-- trigger.action.outText("enter doListConvoys", 30) + local mine = {} + local neutrals = {} + local enemy = {} + local mySide = args.coa + local gID = args.gID + + -- now iterate all convoys, and sort them into bags + for convName, theConvoy in pairs (convoy.convoys) do + if theConvoy.coa == mySide then + table.insert(mine, theConvoy) + elseif theConvoy.coa == 0 then + table.insert(neutrals, theConvoy) -- note: no neutral players + else + table.insert(enemy, theConvoy) + end + end + + -- we now can count each by entry num + -- build report + + local msg = "" + local hasMsg = false + if #mine > 0 then + -- report my own convoys with location + hasMsg = true + msg = msg .. "\nRUNNING ALLIED CONVOYS:\n" + for idx, theConvoy in pairs(mine) do + -- access first group from dict, there is only one + local theGroup = dcsCommon.getFirstItem(theConvoy.groups) + if theGroup and Group.isExist(theGroup) and dcsCommon.getFirstLivingUnit(theGroup) then + local theUnit = dcsCommon.getFirstLivingUnit(theGroup) + msg = msg .. " " .. theConvoy.name .. " enroute to " .. theConvoy.dest + local p = theUnit:getPoint() + local locName, hasLoc = convoy.getLocName(p) + if hasLoc then + msg = msg .. ", now some " .. locName + end + msg = msg .. "\n" + else + msg = msg .. " Lost contact with " .. theConvoy.name .. "\n" + end + end + end + + if convoy.listEnemy and #enemy > 0 then + hasMsg = true + msg = msg .. "\nKNOWN/REPORTED ENEMY CONVOYS:\n" + -- enemy convoys always show closest destObject as destination! + for idx, theConvoy in pairs(enemy) do + local theGroup = dcsCommon.getFirstItem(theConvoy.groups) + if theGroup and Group.isExist(theGroup) then + msg = msg .. " " .. theConvoy.anon .. " enroute to " .. theConvoy.destObject:getName() + if theConvoy.wasAttacked then + local remU = theGroup:getUnits() + msg = msg .. ", " .. #remU .. " units remaining" + end + msg = msg .. ".\n" + if theConvoy.currWP == #theConvoy.waypoints -1 then + msg = msg .. " -=CLOSE TO DESTINATION=-\n" + end + end + end + end + + + if convoy.listNeutral and #neutrals > 0 then + hasMsg = true + msg = msg .. "\nKNOWN NEUTRAL CONVOYS:\n" + -- enemy convoys always show closest destObject as destination! + for idx, theConvoy in pairs(neutrals) do + local theGroup = dcsCommon.getFirstItem(theConvoy.groups) + if theGroup and Group.isExist(theGroup) then + msg = msg .. " " .. theConvoy.name .. " enroute to " .. theConvoy.destObject:getName() .. "\n" + end + end + end + if not hasMsg then + msg = "\nNO CONVOYS.\n" + end + + trigger.action.outTextForGroup(gID, msg, 30) + trigger.action.outSoundForGroup(gID, convoy.actionSound) +end + -- -- UPDATE -- function convoy.update() timer.scheduleFunction(convoy.update, {}, timer.getTime() + 1/convoy.ups) - -- update all master owners + -- check for flags for idx, theZone in pairs (convoy.zones) do ---[[-- local mo = theZone.masterOwner - if mo then - theZone.owner = mo.owner - if theZone.isDynamic then - theZone.coa = mo.owner - end - end --]]-- + if theZone.spawnFlag and + theZone:testZoneFlag(theZone.spawnFlag, "change", "lastSpawnFlag") then + convoy.startConvoy(theZone) + end end end +function convoy.statusUpdate() -- every 10 seconds + timer.scheduleFunction(convoy.statusUpdate, {}, timer.getTime() + 10) + local redNum = 0 + local blueNum = 0 + local neutralNum = 0 + local now = timer.getTime() + local filtered = {} + for convName, theConvoy in pairs (convoy.convoys) do + local hasLosses = false + local groupDead = false + local theZone = theConvoy.origin + local damagedGroup = nil + for gName, theGroup in pairs(theConvoy.groups) do + if Group.isExist(theGroup) then + local newNum = theGroup:getSize() + if newNum < theConvoy.groupSizes[gName] then + hasLosses = true + damagedGroup = theGroup + theConvoy.groupSizes[gName] = newNum + end + if newNum < 1 then + groupDead = true + hasLosses = false + end + else + groupDead = true + end + end + + if hasLosses then + theConvoy.wasAttacked = true + if (now - theConvoy.lastAttackReport) > 300 then -- min 5 minutes between Alerts + if theZone.attackWarnings and damagedGroup then + local theUnit = dcsCommon.getFirstLivingUnit(damagedGroup) + local p = theUnit:getPoint() + local locName, hasLoc = convoy.getLocName(p) + local msg = "Convoy " .. convName .. ", enroute to destination " .. theConvoy.dest .. ", under attack" + if hasLoc then + msg = msg .. " some " .. locName + end + msg = msg .. ", taking losses." + trigger.action.outTextForCoalition(theConvoy.coa, msg, 30) + trigger.action.outSoundForCoalition(theConvoy.coa, convoy.actionSound) + end + theConvoy.lastAttackReport = now + end + convoy.invokeAttackedCallbacks(theConvoy) + theZone = theConvoy.origin + if theZone.attackedOut then + theZone:pollFlag(theZone.attackedOut, "inc") + end + end + + if groupDead then + -- invoke callback + convoy.invokeDestroyedCallbacks(theConvoy) + theZone = theConvoy.origin + if theZone.deadOut then + theZone:pollFlag(theZone.deadOut, "inc") + end + trigger.action.outTextForCoalition(theConvoy.coa, "Convoy " .. convName .. " enroute to " .. theConvoy.dest .. " was destroyed.", 30) + trigger.action.outSoundForCoalition(theConvoy.coa, convoy.actionSound) + if convoy.listEnemy then + local enemy = 1 + if theConvoy.coa == 1 then enemy = 2 end + local msg = "Enemy convoy " .. theConvoy.anon .. " to " .. theConvoy.destObject:getName() .. " destroyed." + trigger.action.outTextForCoalition(enemy, msg, 30) + trigger.action.outSoundForCoalition(enemy, convoy.actionSound) + end + + -- we deallocate after a delay, applies to helos + timer.scheduleFunction(convoy.wipeConvoy, theConvoy, now + theZone.killWipeDelay) + --end + -- do not propagate to filtered + if convoy.verbose then + trigger.action.outText("+++cnvy: filtered <" .. convName .. "> from <" .. theConvoy.origin.name .. "> to <" .. theConvoy.dest .. ">: destroyed", 30) + end + else + -- transfer for next round + if theConvoy.coa == 0 then + neutralNum = neutralNum + 1 + elseif theConvoy.coa == 1 then + redNum = redNum + 1 + else + blueNum = blueNum + 1 + end + filtered[convName] = theConvoy + end + end + convoy.convoys = filtered + if convoy.redConvoy then + cfxZones.setFlagValue(convoy.redConvoy, redNum, convoy) + end + if convoy.blueConvoy then + cfxZones.setFlagValue(convoy.blueConvoy, blueNum, convoy) + end + if convoy.neutralConvoy then + cfxZones.setFlagValue(convoy.neutralConvoy, neutralNum, convoy) + end + if convoy.allConvoy then + cfxZones.setFlagValue(convoy.neutralConvoy, neutralNum + redNum + blueNum, convoy) + end +end -- -- START -- - function convoy.readConfigZone() + convoy.name = "convoyConfig" -- make compatible with dml zones local theZone = cfxZones.getZoneByName("convoyConfig") if not theZone then theZone = cfxZones.createSimpleZone("convoyConfig") end + convoy.actionSound = theZone:getStringFromZoneProperty("actionSound", "UI_SCI-FI_Tone_Bright_Dry_25_stereo.wav") convoy.verbose = theZone.verbose convoy.ups = theZone:getNumberFromZoneProperty("ups", 1) + + convoy.menuName = theZone:getStringFromZoneProperty("menuName", "Convoys") + convoy.hasGUI = theZone:getBoolFromZoneProperty("hasGUI", true) + + convoy.listEnemy = theZone:getBoolFromZoneProperty("listEnemy", true) + convoy.listNeutral = theZone:getBoolFromZoneProperty("listNeutral", true) + if theZone:hasProperty("attachTo:") then + local attachTo = theZone:getStringFromZoneProperty("attachTo:", "") + if radioMenu then -- requires optional radio menu to have loaded + local mainMenu = radioMenu.mainMenus[attachTo] + if mainMenu then + convoy.mainMenu = mainMenu + else + trigger.action.outText("+++convoy: cannot find super menu <" .. attachTo .. ">", 30) + end + else + trigger.action.outText("+++convoy: REQUIRES radioMenu to run before convoy. 'AttachTo:' ignored.", 30) + end + end + + if theZone:hasProperty("redConvoy#") then + convoy.redConvoy = theZone:getStringFromZoneProperty("redConvoy#") + end + if theZone:hasProperty("blueConvoy#") then + convoy.blueConvoy = theZone:getStringFromZoneProperty("blueConvoy#") + end + if theZone:hasProperty("neutralConvoy#") then + convoy.neutralConvoy = theZone:getStringFromZoneProperty("neutralConvoy#") + end + if theZone:hasProperty("allConvoy#") then + convoy.allConvoy = theZone:getStringFromZoneProperty("allConvoy#") + end end function convoy.start() @@ -186,8 +1019,12 @@ function convoy.start() convoy.addConvoyZone(aZone) -- add to list end + -- connect event handler + world.addEventHandler(convoy) + -- start update timer.scheduleFunction(convoy.update, {}, timer.getTime() + 1/convoy.ups) + convoy.statusUpdate() -- start all zones that have onstart for gName, theZone in pairs(convoy.zones) do @@ -195,6 +1032,8 @@ function convoy.start() convoy.startConvoy(theZone) end end + -- say Hi! + trigger.action.outText("cf/x Convoy v" .. convoy.version .. " started.", 30) return true end @@ -213,7 +1052,15 @@ attacked signal each time a unit is destroyed importantType - type that must survive= coalition / masterOwner isActive# 0/1 -can only have one active convoy -can it have helicopters? +doWipe? to wipe all my convoys? +tacTypes = desinate units types that must survive. Upon start, ensure that at least one tac type is pressenr +when arriving, verify that it still is, or fail earlier when all tactypes are destroyed. +convoy status UI +do: +when escort engages, send notice +when escort damaged, send notice +mark source and dest of convoy on map for same side +make routes interchangeable between convoys? +make inf units disembark when convoy attacked --]]-- \ No newline at end of file diff --git a/modules/csarManager2.lua b/modules/csarManager2.lua index 7c46503..f8e92e4 100644 --- a/modules/csarManager2.lua +++ b/modules/csarManager2.lua @@ -1,5 +1,5 @@ csarManager = {} -csarManager.version = "4.0.0" +csarManager.version = "4.2.1" csarManager.ups = 1 --[[-- VERSION HISTORY @@ -46,7 +46,11 @@ csarManager.ups = 1 3.4.0 - global timeLimit option in config zone - fixes expiration bug when persisting data 4.0.0 - support for mainMenu - + 4.0.1 - increased verbosity + - fix for Jul-11 2024 DCS bugs + 4.1.0 - support for DCS 2.9.6 dynamic spawns + 4.2.0 - automatically support twn if present + 4.2.1 - added Chinook to csar default set (via common) INTEGRATES AUTOMATICALLY WITH playerScore INTEGRATES WITH LIMITED AIRFRAMES @@ -64,6 +68,8 @@ csarManager.requiredLibs = { -- unitConfigs contain the config data for any helicopter -- currently in the game. The Array is indexed by unit name +-- requires single-unit player groups +-- compatible with DCS 2.9.6 dcs dynamic spawns csarManager.unitConfigs = {} -- @@ -181,7 +187,6 @@ function csarManager.createCSARMissionData(point, theSide, freq, name, numCrew, end if csarManager.useRanks then --- local ranks = csarManager.ranks -- {"Lt", "Lt", "Lt", "Col", "Cpt", "WO", "WO"} local myRank = dcsCommon.pickRandom(csarManager.ranks) name = myRank .. " " .. name end @@ -243,7 +248,6 @@ function csarManager.removeMission(theMission, pickup) if aMission ~= theMission then table.insert(newMissions, aMission) else --- csarManager.invokeRemovedMissionCallbacks(theMission) if pickup then csarManager.invokePickUpCallbacks(theMission) end @@ -296,6 +300,7 @@ end function csarManager.getUnitConfig(theUnit) -- will create new config if not existing +-- compatible with dynamic spawns for DCS 2.9.6 if not theUnit then trigger.action.outText("+++csar: nil unit in get config!", 30) return nil @@ -331,15 +336,22 @@ function csarManager:onEvent(event) if not dcsCommon.isPlayerUnit(theUnit) then return end -- not a player unit + if csarManager.verbose then + trigger.action.outText("csarM: player event <" .. event.id .. "> -- (=<" .. dcsCommon.event2text(event.id) .. ">)", 30) + 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 local ID = event.id - if ID == 4 then -- landed + if ID == 4 or ID == 55 then -- landed, runway touch + if csarManager.verbose then + trigger.action.outText("land event " .. ID .. "received.", 30) + end csarManager.heloLanded(theUnit) end - if ID == 3 or ID == 55 then -- take off, postponed take-off + if ID == 3 or ID == 54 then -- take off, runway take-off csarManager.heloDeparted(theUnit) end @@ -354,18 +366,15 @@ function csarManager:onEvent(event) csarManager.setCommsMenu(theUnit) -- we also need to make sure that there are no -- more troopsOnBoard - local myName = theUnit:getName() local conf = csarManager.getUnitConfig(theUnit) conf.unit = theUnit conf.troopsOnBoard = {} local totalMass = cargoSuper.calculateTotalMassFor(myName) - -- now also set cargo weight for the unit cargoSuper.removeAllMassForCargo(myName, "Evacuees") -- will allocate new empty table totalMass = cargoSuper.calculateTotalMassFor(myName) trigger.action.setUnitInternalCargo(myName, totalMass) -- super recalcs - end end @@ -698,6 +707,8 @@ end function csarManager.setCommsMenu(theUnit) + -- add menu for this aircraft/group when it spawns + -- compatible with dynamic spawns for DCS 2.9.6 if not theUnit then return end if not theUnit:isExist() then return end @@ -821,11 +832,19 @@ function csarManager.doListCSARRequests(args) status = status .. " [" .. delta .. "]" -- remove me end end + local locinfo = "" + if twn and towns then + local village, data, dist = twn.closestTownTo(mission.zone.point) + dist = dist * 0.539957 -- nm conversion + dist = math.floor(dist/100) / 10 + local bear = dcsCommon.compassPositionOfARelativeToB(mission.zone.point, data.p) + locinfo = ", " .. dist .. "nm " .. bear .. " of " .. village + end if csarManager.vectoring then - report = report .. "\n".. mission.name .. ", bearing " .. b .. ", " ..mission.dist .."nm, " .. " ADF " .. mission.freq * 10 .. " kHz - " .. status + report = report .. "\n".. mission.name .. locinfo .. ", bearing " .. b .. ", " ..mission.dist .."nm, " .. " ADF " .. mission.freq * 10 .. " kHz - " .. status else -- leave out vectoring - report = report .. "\n".. mission.name .. " ADF " .. mission.freq * 10 .. " kHz - " .. status + report = report .. "\n".. mission.name .. locinfo .. " ADF " .. mission.freq * 10 .. " kHz - " .. status end end end diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index 9c3f0db..04ddac7 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "3.0.9" +dcsCommon.version = "3.1.2" --[[-- VERSION HISTORY 3.0.0 - removed bad bug in stringStartsWith, only relevant if caseSensitive is false - point2text new intsOnly option @@ -26,7 +26,9 @@ dcsCommon.version = "3.0.9" 3.0.9 - new getOrigPositionByID() - unitName2ID[] reverse lookup - unitName2Heading - +3.1.0 - updates to events, DCS update 7-11 2024 hardening +3.1.1 - added Chinook to troop carriers +3.1.2 - isTroopCarrier() hardening against DCS sillieness --]]-- -- dcsCommon is a library of common lua functions @@ -39,7 +41,7 @@ dcsCommon.version = "3.0.9" -- globals dcsCommon.cbID = 0 -- callback id for simple callback scheduling - dcsCommon.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P", "OH58D"} -- Ka-50, Apache and Gazelle can't carry troops + dcsCommon.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P", "OH58D", "CH-47Fbl1"} -- Ka-50, Apache and Gazelle can't carry troops dcsCommon.coalitionSides = {0, 1, 2} dcsCommon.maxCountry = 86 -- number of countries defined in total @@ -707,7 +709,7 @@ dcsCommon.version = "3.0.9" end function dcsCommon.compassPositionOfARelativeToB(A, B) - -- warning: is REVERSE in order for bearing, returns a string like 'Sorth', 'Southwest' + -- warning: is REVERSE in order for bearing, returns a string like 'North', 'Southwest' if not A then return "***error:A***" end if not B then return "***error:B***" end local bearing = dcsCommon.bearingInDegreesFromAtoB(B, A) -- returns 0..360 @@ -2566,9 +2568,10 @@ end "Pilot Suicide", "player cap airfield", "emergency landing", "unit create task", -- 44 "unit delete task", "Simulation start", "weapon rearm", "weapon drop", -- 48 "unit task timeout", "unit task stage", -- 50 - "subtask score", "extra score", "mission restart", "winner", - "postponed takeoff", "postponed land", -- 56 - "max"} + "subtask score", "mission restart", "winner", -- 53 + "runway takeoff", "runway touchdown", "LMS Restart", -- 56 + "sim freeze", "sum unfreeze", "player start repair", "player end repair", --60 + "max",} -- 61 if id > #events then return "Unknown (ID=" .. id .. ")" end return events[id] end @@ -2839,9 +2842,9 @@ end function dcsCommon.isTroopCarrier(theUnit, carriers) -- return true if conf can carry troups if not theUnit then return false end - + if not theUnit.getTypeName then return false end -- hardening against DCS sillieness -- see if carriers contains "helo" and theUnit is a helo - if dcsCommon.arrayContainsString(carriers, "helo") or dcsCommon.arrayContainsString(carriers, "helos")then + if dcsCommon.arrayContainsString(carriers, "helo") or dcsCommon.arrayContainsString(carriers, "helos") then local grp = theUnit:getGroup() if grp:getCategory() == 1 then -- NOT category bug prone, is a group check return true diff --git a/modules/heloTroops.lua b/modules/heloTroops.lua index 66c146a..f8684a6 100644 --- a/modules/heloTroops.lua +++ b/modules/heloTroops.lua @@ -1,5 +1,5 @@ cfxHeloTroops = {} -cfxHeloTroops.version = "3.0.4" +cfxHeloTroops.version = "3.1.0" cfxHeloTroops.verbose = false cfxHeloTroops.autoDrop = true cfxHeloTroops.autoPickup = false @@ -8,31 +8,6 @@ cfxHeloTroops.requestRange = 500 -- meters -- --[[-- VERSION HISTORY - 1.1.3 - repaired forgetting 'wait-' when loading/disembarking - 1.1.4 - corrected coalition bug in deployTroopsFromHelicopter - 2.0.0 - added weight change when troops enter and leave the helicopter - - idividual troop capa max per helicopter - 2.0.1 - lib loader verification - - uses dcsCommon.isTroopCarrier(theUnit) - 2.0.2 - can now deploy from spawners with "requestable" attribute - 2.1.0 - supports config zones - - check spawner legality by types - - updated types to include 2.7.6 additions to infantry - - updated types to include stinger/manpads - 2.2.0 - minor maintenance (dcsCommon) - - (re?) connected readConfigZone (wtf?) - - persistence support - - made legalTroops entrirely optional and defer to dcsComon else - 2.3.0 - interface with owned zones and playerScore when - - combat-dropping troops into non-owned owned zone. - - prevent auto-load from pre-empting loading csar troops - 2.3.1 - added ability to self-define troopCarriers via config - 2.4.0 - added missing support for attackZone orders (destination) - - eliminated cfxPlayer module import and all dependencies - - added support for groupTracker / limbo - - removed restriction to only apply to helicopters in anticipation of the C-130 Hercules appearing in the game - 2.4.1 - new actionSound attribute, sound plays to group whenever - troops have boarded or disembarked 3.0.0 - added requestable cloner support - harmonized spawning invocations across cloners and spawners - dmlZones @@ -41,14 +16,10 @@ cfxHeloTroops.requestRange = 500 -- meters 3.0.2 - fixed a typo in in-air menu 3.0.3 - pointInZone check for insertion rather than radius 3.0.4 - also handles picking up troops with orders "captureandhold" + 3.0.5 - worked around a new issues accessing a unit's name + 3.1.0 - compatible with DCS 2.9.6 dynamic spawning --]]-- --- --- cfxHeloTroops -- a module to pick up and drop infantry. --- Can be used with ANY aircraft, configured by default to be --- restricted to troop-carrying helicopters. --- might be configure to apply to any type you want using the --- configuration zone. cfxHeloTroops.requiredLibs = { @@ -284,9 +255,8 @@ function cfxHeloTroops.addConfigMenu(conf) end function cfxHeloTroops.setCommsMenu(theUnit) - -- depending on own load state, we set the command structure - -- it begins at 10-other, and has 'Assault Troops' as main menu with submenus - -- as required + -- compatible with DCS 2.9.6 dynamic spawns + -- set F10 Other.. menu for group if not theUnit then return end if not theUnit:isExist() then return end @@ -301,7 +271,7 @@ function cfxHeloTroops.setCommsMenu(theUnit) local group = theUnit:getGroup() local id = group:getID() local conf = cfxHeloTroops.getUnitConfig(theUnit) - conf.id = id; -- we do this ALWAYS to it is current even after a crash + conf.id = id; -- we ALWAYS do this so it is current even after a crash conf.unit = theUnit -- link back -- ok, first, if we don't have an F-10 menu, create one @@ -819,10 +789,10 @@ function cfxHeloTroops:onEvent(theEvent) local initiator = theEvent.initiator if not initiator then return end -- not interested local theUnit = initiator - local name = theUnit:getName() -- see if this is a player aircraft if not theUnit.getPlayerName then return end -- not a player if not theUnit:getPlayerName() then return end -- not a player + local name = theUnit:getName() -- moved to a later -- only for helicopters -- overridedden by troop carriers -- we don't check for cat any more, so any airframe diff --git a/modules/jtacGrpUI.lua b/modules/jtacGrpUI.lua index a8dc9c4..95e7550 100644 --- a/modules/jtacGrpUI.lua +++ b/modules/jtacGrpUI.lua @@ -1,5 +1,5 @@ jtacGrpUI = {} -jtacGrpUI.version = "3.0.0" +jtacGrpUI.version = "3.1.0" jtacGrpUI.requiredLibs = { "dcsCommon", -- always "cfxZones", @@ -12,6 +12,7 @@ jtacGrpUI.requiredLibs = { - clean-up - jtacSound 3.0.0 - support for attachTo: + 3.1.0 - support for DCS 2.0.6 dynamic player spwans --]]-- -- find & command cfxGroundTroops-based jtacs @@ -151,7 +152,7 @@ function jtacGrpUI.setCommsMenu(theGroup) end local conf = jtacGrpUI.getConfigForGroup(theGroup) - conf.id = theGroup:getID(); -- we always do this ALWAYS + conf.id = theGroup:getID(); -- we always do this if jtacGrpUI.simpleCommands then -- we install directly in F-10 other @@ -294,25 +295,29 @@ function jtacGrpUI:onEvent(theEvent) if not theEvent then return end local theUnit = theEvent.initiator if not theUnit then return end + if not theUnit.getName then return end -- dcs 2.9.6 jul-11 fix local uName = theUnit:getName() if not theUnit.getPlayerName then return end if not theUnit:getPlayerName() then return end - -- we now have a player birth event. - local pName = theUnit:getPlayerName() - local theGroup = theUnit:getGroup() - if not theGroup then return end - local gName = theGroup:getName() - if not gName then return end - if jtacGrpUI.verbose then - trigger.action.outText("+++jGUI: birth player. installing JTAC for <" .. pName .. "> on unit <" .. uName .. ">", 30) + local id = theEvent.id + if id == 15 then + -- we now have a player birth event. + local pName = theUnit:getPlayerName() + local theGroup = theUnit:getGroup() + if not theGroup then return end + local gName = theGroup:getName() + if not gName then return end + if jtacGrpUI.verbose then + trigger.action.outText("+++jGUI: birth player. installing JTAC for <" .. pName .. "> on unit <" .. uName .. ">", 30) + end + local conf = jtacGrpUI.getConfigByGroupName(gName) + if conf then + jtacGrpUI.removeCommsFromConfig(conf) -- remove menus + jtacGrpUI.resetConfig(conf) -- re-init this group for when it re-appears + end + + jtacGrpUI.setCommsMenu(theGroup) end - local conf = jtacGrpUI.getConfigByGroupName(gName) - if conf then - jtacGrpUI.removeCommsFromConfig(conf) -- remove menus - jtacGrpUI.resetConfig(conf) -- re-init this group for when it re-appears - end - - jtacGrpUI.setCommsMenu(theGroup) end diff --git a/modules/messenger.lua b/modules/messenger.lua index d1a2138..243faf7 100644 --- a/modules/messenger.lua +++ b/modules/messenger.lua @@ -1,5 +1,5 @@ messenger = {} -messenger.version = "3.1.0" +messenger.version = "3.2.0" messenger.verbose = false messenger.requiredLibs = { "dcsCommon", -- always @@ -13,6 +13,8 @@ messenger.messengers = {} 3.0.0 - removed messenger, in?, f? attributes, harmonized on messenger? 3.1.0 - msgGroup supports multiple groups, separated by comma - msgUnit supports multiple units, separated by comma + 3.2.0 - loc and twn wildcard support (inherited from zones) + --]]-- function messenger.addMessenger(theZone) @@ -165,7 +167,7 @@ end -- --- reat attributes +-- read attributes -- function messenger.createMessengerWithZone(theZone) local aMessage = theZone:getStringFromZoneProperty("message", "") diff --git a/modules/ownAll.lua b/modules/ownAll.lua index f58ac61..f0e92cf 100644 --- a/modules/ownAll.lua +++ b/modules/ownAll.lua @@ -1,5 +1,5 @@ ownAll = {} -ownAll.version = "1.0.0" +ownAll.version = "1.1.0" ownAll.verbose = false ownAll.requiredLibs = { "dcsCommon", -- always @@ -9,7 +9,7 @@ ownAll.requiredLibs = { --[[-- VERSION HISTORY - 1.0.0 - Initial version - + - 1.1.0 - dml:masterOwner / getCoalition() updates --]]-- ownAll.zones = {} @@ -69,13 +69,13 @@ function ownAll.calcState(theZone) local blueNum = 0 local allSame = true if #theZone.zones < 1 then return -1, 0, 0 end - local s = theZone.zones[1].owner + local s = theZone.zones[1]:getCoalition() -- owner if not s then trigger.action.outText("+++oAll: zone <" .. theZone.zones[1].name .."> has no owner (?)", 30) s = -1 end for idx, aZone in pairs (theZone.zones) do - local s2 = aZone.owner + local s2 = aZone:getCoalition() -- .owner if not s2 then trigger.action.outText("+++oAll: zone <" .. aZone.name .."> has no owner (?)", 30) s2 = -1 diff --git a/modules/ownedZones.lua b/modules/ownedZones.lua index 9b1668a..bfd1b00 100644 --- a/modules/ownedZones.lua +++ b/modules/ownedZones.lua @@ -1,5 +1,5 @@ cfxOwnedZones = {} -cfxOwnedZones.version = "2.3.1" +cfxOwnedZones.version = "2.4.0" cfxOwnedZones.verbose = false cfxOwnedZones.announcer = true cfxOwnedZones.name = "cfxOwnedZones" @@ -43,6 +43,8 @@ cfxOwnedZones.name = "cfxOwnedZones" - title attribute - code clean-up 2.3.1 - restored getNearestOwnedZoneToPoint +2.4.0 - dmlZones masterOwner update + --]]-- cfxOwnedZones.requiredLibs = { "dcsCommon", @@ -180,6 +182,7 @@ function cfxOwnedZones.addOwnedZone(aZone) aZone.neutralFill = aZone:getRGBAVectorFromZoneProperty("neutralFill", cfxOwnedZones.neutralFill) -- masterOwner +--[[-- if aZone:hasProperty("masterOwner") then local masterZone = aZone:getStringFromZoneProperty("masterOwner", "cfxNoneErr") local theMaster = cfxZones.getZoneByName(masterZone) @@ -193,6 +196,7 @@ function cfxOwnedZones.addOwnedZone(aZone) end end end +--]]-- aZone.announcer = aZone:getBoolFromZoneProperty("announcer", cfxZones.announcer) if aZone:hasProperty("announce") then @@ -348,7 +352,7 @@ function cfxOwnedZones.update() for idz, theZone in pairs(cfxOwnedZones.zones) do theZone.numRed = 0 theZone.numBlue = 0 - local lastOwner = theZone.owner + local lastOwner = theZone.owner -- do NOT use dml:getCoalition()! if not lastOwner then trigger.action.outText("+++owdZ: WARNING - zone <" .. theZone.name .. "> has NIL owner", 30) return @@ -472,7 +476,7 @@ function cfxOwnedZones.update() -- we do nothing elseif theZone.masterOwner then -- inherit from my master - newOwner = theZone.masterOwner.owner + newOwner = theZone:getCoalition() -- theZone.masterOwner.owner elseif theZone.numRed < 1 and theZone.numBlue < 1 then -- no troops here. Become neutral? if theZone.numKeep < 1 then diff --git a/modules/playerScore.lua b/modules/playerScore.lua index a13472b..e3d0160 100644 --- a/modules/playerScore.lua +++ b/modules/playerScore.lua @@ -1,5 +1,5 @@ cfxPlayerScore = {} -cfxPlayerScore.version = "3.3.0" +cfxPlayerScore.version = "3.3.1" cfxPlayerScore.name = "cfxPlayerScore" -- compatibility with flag bangers cfxPlayerScore.badSound = "Death BRASS.wav" cfxPlayerScore.scoreSound = "Quest Snare 3.wav" @@ -16,6 +16,8 @@ cfxPlayerScore.firstSave = true -- to force overwrite 3.1.0 - shared data for persistence 3.2.0 - integration with bank 3.3.0 - case INsensitivity for all typeScore objects + 3.3.1 - fixes for DCS oddity in events after update + - cleanup --]]-- cfxPlayerScore.requiredLibs = { @@ -104,15 +106,12 @@ function cfxPlayerScore.featsForLocation(name, loc, coa, featType, killer, victi if theZone.featNum == 0 then canAward = false end - if theZone.featType ~= featType then canAward = false end - if not (theZone.coalition == 0 or theZone.coalition == coa) then canAward = false end - if featType == "PVP" then -- make sure kill is pvp kill if not victim then canAward = false @@ -122,22 +121,17 @@ function cfxPlayerScore.featsForLocation(name, loc, coa, featType, killer, victi canAward = false end end - if not cfxZones.pointInZone(loc, theZone) then canAward = false end - if theZone.ppOnce then if theZone.awardedTo[name] then canAward = false end end - if canAward then - table.insert(theFeats, theZone) -- jupp, add it - else + table.insert(theFeats, theZone) -- jupp, add it end - end return theFeats end @@ -166,7 +160,11 @@ function cfxPlayerScore.preprocessWildcards(inMsg, aUnit, aVictim) theMsg = theMsg:gsub("", "unknown AI") end end - theMsg = theMsg:gsub("", aVictim:getName()) + if aVictim.getName then + theMsg = theMsg:gsub("", aVictim:getName()) + else + theMsg = theMsg:gsub("", "*?*") -- dcs oddity + end theMsg = theMsg:gsub("", aVictim:getTypeName()) -- victim may not have group. guard against that -- happens if unit 'cooks off' @@ -205,16 +203,15 @@ function cfxPlayerScore.cat2BaseScore(inCat) if inCat == 2 then return cfxPlayerScore.ground end -- ground if inCat == 3 then return cfxPlayerScore.ship end -- ship if inCat == 4 then return cfxPlayerScore.train end -- train - trigger.action.outText("+++scr c2bs: unknown category for lookup: <" .. inCat .. ">, returning 1", 30) - return 1 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() + local inName + if inVictim.getName then inName = inVictim:getName() else inName = "*?*" end -- dcs oddity if cfxPlayerScore.verbose then trigger.action.outText("+++PScr: ob2sc entry to resolve name <" .. inName .. ">", 30) end @@ -251,13 +248,10 @@ function cfxPlayerScore.object2score(inVictim, killSide) -- does not have group objectScore = cfxPlayerScore.typeScore[theType:upper()] end end - if type(objectScore) == "string" then objectScore = tonumber(objectScore) end - if objectScore then return objectScore end - -- we now try and get the general type of the killed object local desc = inVictim:getDesc() -- Object.getDesc(inVictim) local attributes = desc.attributes @@ -268,7 +262,6 @@ function cfxPlayerScore.object2score(inVictim, killSide) -- does not have group if attributes["Ships"] then return cfxPlayerScore.ship end -- trains can't be detected end - if not objectScore then return 0 end return objectScore end @@ -277,34 +270,29 @@ function cfxPlayerScore.unit2score(inUnit) local vicGroup = inUnit:getGroup() local vicCat = vicGroup:getCategory()-- group cat, not 2.9 affected local vicType = inUnit:getTypeName() - local vicName = inUnit:getName() + local vicName + if inUnit.getName then vicName = inUnit:getName() else vicName = "*?*" end if type(vicName) == "number" then vicName = tostring(vicName) end -- simply extend by adding items to the typescore table.concat -- we first try by unit name. This allows individual -- named hi-value targets to have individual scores local uScore = cfxPlayerScore.typeScore[vicName:upper()] - -- see if all members of group score if (not uScore) and vicGroup then local grpName = vicGroup:getName() uScore = cfxPlayerScore.typeScore[grpName:upper()] end - if not uScore then -- WE NOW TRY TO ACCESS BY VICTIM'S TYPE STRING uScore = cfxPlayerScore.typeScore[vicType:upper()] - else - end if type(uScore) == "string" then -- convert string to number uScore = tonumber(uScore) end - if not uScore then uScore = 0 end if uScore > 0 then return uScore end - -- only apply base scores when the lookup did not give a result uScore = cfxPlayerScore.cat2BaseScore(vicCat) return uScore @@ -312,7 +300,7 @@ end function cfxPlayerScore.getPlayerScore(playerName) local thePlayerScore = cfxPlayerScore.playerScore[playerName] - if thePlayerScore == nil then + if not thePlayerScore then thePlayerScore = {} thePlayerScore.name = playerName thePlayerScore.score = 0 -- score @@ -379,7 +367,6 @@ function cfxPlayerScore.doLogTypeKill(playerName, thePlayerScore, theType) killCount = killCount + 1 thePlayerScore.totalKills = thePlayerScore.totalKills + 1 thePlayerScore.killTypes[theType] = killCount - cfxPlayerScore.setPlayerScore(playerName, thePlayerScore) end @@ -390,14 +377,12 @@ function cfxPlayerScore.logKillForPlayer(playerName, theUnit) if not playerName then return end local thePlayerScore = cfxPlayerScore.getPlayerScore(playerName) local theType = theUnit:getTypeName() - if cfxPlayerScore.deferred then -- just queue it table.insert(thePlayerScore.killQueue, theType) cfxPlayerScore.setPlayerScore(playerName, thePlayerScore) -- write-through. why? because it may be a new entry. return end - cfxPlayerScore.doLogTypeKill(playerName, thePlayerScore, theType) end @@ -410,9 +395,7 @@ function cfxPlayerScore.doLogFeat(playerName, thePlayerScore, theFeat) featCount = featCount + 1 thePlayerScore.totalFeats = thePlayerScore.totalFeats + 1 thePlayerScore.featTypes[theFeat] = featCount - cfxPlayerScore.setPlayerScore(playerName, thePlayerScore) - end function cfxPlayerScore.logFeatForPlayer(playerName, theFeat, coa) @@ -421,34 +404,29 @@ function cfxPlayerScore.logFeatForPlayer(playerName, theFeat, coa) if not theFeat then return end if not playerName then return end -- access player's record. will alloc if new by itself - if coa then local disclaim = "" if cfxPlayerScore.deferred then disclaim = " (award pending)" end trigger.action.outTextForCoalition(coa, playerName .. " achieved " .. theFeat .. disclaim, 30) trigger.action.outSoundForCoalition(coa, cfxPlayerScore.scoreSound) end - local thePlayerScore = cfxPlayerScore.getPlayerScore(playerName) if cfxPlayerScore.deferred then table.insert(thePlayerScore.featQueue, theFeat) cfxPlayerScore.setPlayerScore(playerName, thePlayerScore) return end - cfxPlayerScore.doLogFeat(playerName, thePlayerScore, theFeat) end function cfxPlayerScore.playerScore2text(thePlayerScore, scoreOnly) if not scoreOnly then scoreOnly = false end local desc = thePlayerScore.name .. " statistics:\n" - if cfxPlayerScore.reportScore then desc = desc .. " - score: ".. thePlayerScore.score .. " - total kills: " .. thePlayerScore.totalKills .. "\n" if scoreOnly then return desc end - -- now go through all kills desc = desc .. "\nKills by type:\n" if dcsCommon.getSizeOfTable(thePlayerScore.killTypes) < 1 then @@ -474,16 +452,13 @@ function cfxPlayerScore.playerScore2text(thePlayerScore, scoreOnly) desc = desc .. "\n" end end - if cfxPlayerScore.reportScore and thePlayerScore.scoreaccu > 0 then desc = desc .. "\n - unclaimed score: " .. thePlayerScore.scoreaccu .."\n" end - local featCount = dcsCommon.getSizeOfTable(thePlayerScore.featQueue) if cfxPlayerScore.reportFeats and featCount > 0 then desc = desc .. " - unclaimed feats: " .. featCount .."\n" end - return desc end @@ -508,7 +483,6 @@ function cfxPlayerScore.scoreSummaryForPlayersOfCoalition(side) if count < 1 then desc = desc .. " (No score yet)" end - desc = desc .. "\n" return desc end @@ -537,17 +511,14 @@ function cfxPlayerScore.scoreTextForAllPlayers(ranked) isFirst = false rank = rank + 1 end - if dcsCommon.getSizeOfTable(theScores) < 1 then theText = theText .. " (No score yet)\n" end - if cfxPlayerScore.reportCoalition then --theText = theText .. "\n" theText = theText .. "\nRED total: " .. cfxPlayerScore.coalitionScore[1] theText = theText .. "\nBLUE total: " .. cfxPlayerScore.coalitionScore[2] end - return theText end @@ -560,7 +531,11 @@ function cfxPlayerScore.isNamedUnit(theUnit) else -- WARNING: NO EXIST CHECK DONE! -- after kill, unit is dead, so will no longer exist! - theName = theUnit:getName() + if theUnit.getName then + theName = theUnit:getName() + else + theName = "*?*" + end if not theName then return false end end if cfxPlayerScore.typeScore[theName:upper()] then @@ -576,9 +551,7 @@ function cfxPlayerScore.awardScoreTo(killSide, theScore, killerName) else playerScore = cfxPlayerScore.updateScoreForPlayer(killerName, theScore) end - if not cfxPlayerScore.reportScore then return end - if cfxPlayerScore.announcer then if (theScore > 0) and cfxPlayerScore.deferred then thePlayerRecord = cfxPlayerScore.getPlayerScore(killerName) -- re-read after write @@ -614,9 +587,9 @@ function cfxPlayerScore.preProcessor(theEvent) if theEvent.initiator == nil then return false 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 @@ -627,19 +600,16 @@ function cfxPlayerScore.preProcessor(theEvent) theEvent.id == 30 or -- unit loss theEvent.id == 6 then -- eject -- these can lead to a pilot demerit - --trigger.action.outText("PREPROC plane player extra event - possible death", 30) -- event does NOT have a player cfxPlayerScore.handlePlayerDeath(theEvent) return false end end - -- initiator must be player if not theUnit.getPlayerName or not theUnit:getPlayerName() then return false end - if theEvent.id == 28 then -- we only are interested in kill events where -- there is a target @@ -650,20 +620,17 @@ function cfxPlayerScore.preProcessor(theEvent) 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 @@ -686,7 +653,7 @@ function cfxPlayerScore.preProcessor(theEvent) -- take off. overwrites timestamp for last landing -- so a blipping t/o does nor count. Pre-proc only - if theEvent.id == 3 then + if theEvent.id == 3 or theEvent.id == 54 then local now = timer.getTime() local playerName = theUnit:getPlayerName() cfxPlayerScore.lastPlayerLanding[playerName] = now -- overwrite @@ -696,7 +663,7 @@ function cfxPlayerScore.preProcessor(theEvent) -- landing can score. but only the first landing in x seconds -- landing in safe zone promotes any queued scores to -- permanent if enabled, then nils queue - if theEvent.id == 4 then + if theEvent.id == 4 or theEvent.id == 55 then -- player landed. filter multiple landed events local now = timer.getTime() local playerName = theUnit:getPlayerName() @@ -704,7 +671,7 @@ function cfxPlayerScore.preProcessor(theEvent) 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.", 30) + 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 @@ -731,23 +698,18 @@ function cfxPlayerScore.checkKillFeat(name, killer, victim, fratricide) if not fratricide then fratricide = false end local theLoc = victim:getPoint() -- vic's loc is relevant for zone check local coa = killer:getCoalition() - local killFeats = cfxPlayerScore.featsForLocation(name, theLoc, coa,"KILL", killer, victim) - if (not fratricide) and #killFeats > 0 then -- use the feat description -- we may want to use closest, currently simply the first theFeatZone = killFeats[1] local desc = cfxPlayerScore.evalFeatDescription(name, theFeatZone, killer, victim) -- updates awardedTo - cfxPlayerScore.logFeatForPlayer(name, desc, playerSide) theScore = cfxPlayerScore.getPlayerScore(name) -- re-read after write - if cfxPlayerScore.verbose then trigger.action.outText("Kill feat awarded/queued for <" .. name .. ">", 30) end end - end function cfxPlayerScore.killDetected(theEvent) @@ -767,7 +729,6 @@ function cfxPlayerScore.killDetected(theEvent) -- was it a player kill? local pk = dcsCommon.isPlayerUnit(victim) - -- was it a scenery object? local wasBuilding = dcsCommon.isSceneryObject(victim) if wasBuilding then @@ -799,7 +760,9 @@ function cfxPlayerScore.killDetected(theEvent) --if not victim.getGroup then if isStO then -- static objects have no group - local staticName = victim:getName() -- on statics, this returns + local staticName + if victim.getName then staticName = victim:getName() -- on statics, this returns + else staticName = "*?*" end -- name as entered in TOP LINE local staticScore = cfxPlayerScore.object2score(victim, killSide) @@ -817,7 +780,6 @@ function cfxPlayerScore.killDetected(theEvent) else -- no score, no mentions end - if not fraternicide then cfxPlayerScore.checkKillFeat(killerName, killer, victim, false) end @@ -834,12 +796,7 @@ function cfxPlayerScore.killDetected(theEvent) trigger.action.outText("+++scr: strange stuff:cat, outta here", 30) return end - local unitScore = cfxPlayerScore.unit2score(victim) - - -- see which weapon was used. gun kills score 2x --- local killMeth = "" -- meth is currently not defined --- local killWeap = theEvent.weapon -- not supported either - + local unitScore = cfxPlayerScore.unit2score(victim) if pk then -- player kill - add player's name vicDesc = victim:getPlayerName() .. " in " .. vicDesc scoreMod = scoreMod * cfxPlayerScore.pkMod @@ -869,17 +826,14 @@ function cfxPlayerScore.killDetected(theEvent) trigger.action.outTextForCoalition(killSide, killerName .. " reports killing strategic unit '" .. victim:getName() .. "'", 30) end end - local totalScore = unitScore * scoreMod -- if the score is negative, awardScoreTo will automatically -- make it immediate, else depending on deferred cfxPlayerScore.awardScoreTo(killSide, totalScore, killerName) - if not fraternicide then -- only award kill feats for kills of the enemy cfxPlayerScore.checkKillFeat(killerName, killer, victim, false) end - end function cfxPlayerScore.handlePlayerLanding(theEvent) @@ -890,13 +844,11 @@ function cfxPlayerScore.handlePlayerLanding(theEvent) if cfxPlayerScore.verbose then trigger.action.outText("+++pScr: Player <" .. playerName .. "> landed", 30) end - local theScore = cfxPlayerScore.getPlayerScore(playerName) - -- see if a feat is available for this landing local landingFeats = cfxPlayerScore.featsForLocation(playerName, theLoc, playerSide,"LANDING") - -- first, scheck if landing is awardable, and if so, + -- first, check if landing is awardable, and if so, -- award the landing if cfxPlayerScore.landing > 0 or #landingFeats > 0 then -- yes, landings are awarded a score. do before @@ -914,7 +866,6 @@ function cfxPlayerScore.handlePlayerLanding(theEvent) desc = desc .. " aircraft" end end - cfxPlayerScore.updateScoreForPlayer(playerName, cfxPlayerScore.landing) cfxPlayerScore.logFeatForPlayer(playerName, desc, playerSide) theScore = cfxPlayerScore.getPlayerScore(playerName) -- re-read after write @@ -922,7 +873,6 @@ function cfxPlayerScore.handlePlayerLanding(theEvent) trigger.action.outText("Landing feat awarded/queued for <" .. playerName .. ">", 30) end end - -- see if we are using deferred scoring, else can end right now if not cfxPlayerScore.deferred then return @@ -930,7 +880,6 @@ function cfxPlayerScore.handlePlayerLanding(theEvent) -- only continue if there is anything to award local killSize = dcsCommon.getSizeOfTable(theScore.killQueue) local featSize = dcsCommon.getSizeOfTable(theScore.featQueue) - if cfxPlayerScore.verbose then trigger.action.outText("+++pScr: prepping deferred score for <" .. playerName ..">", 30) end @@ -948,15 +897,17 @@ function cfxPlayerScore.handlePlayerLanding(theEvent) if (theZone.owner == coa) or (theZone.owner == 0) or (theZone.owner == nil) then if cfxZones.pointInZone(loc, theZone) then isSafe = true + if cfxPlayerScore.verbose then + trigger.action.outText("+++pScr: Zone <" .. theZone.name .. ">: owner=<" .. theZone.owner .. ">, my coa=<" .. coa .. ">, LANDED SAFELY", 30) + end end else - if cfxPlayerScore.verbose then - trigger.action.outText("+++pSc: Zone <" .. theZone.name .. ">: owner=<" .. theZone.owner .. ">, my coa=<" .. coa .. ">, no owner match") + if cfxPlayerScore.verbose and cfxZones.pointInZone(loc, theZone) then + trigger.action.outText("+++pScr: Zone <" .. theZone.name .. ">: owner=<" .. theZone.owner .. ">, player unit <" .. theUnit:getName() .. ">, my coa=<" .. coa .. ">, no owner match", 30) end end end end - if not isSafe then if cfxPlayerScore.verbose then trigger.action.outText("+++pScr: deferred, but not inside score safe zone.", 30) @@ -975,7 +926,6 @@ function cfxPlayerScore.scheduledAward(args) -- called with player name and unit name in args local playerName = args[1] local unitName = args[2] - local theUnit = Unit.getByName(unitName) if not theUnit or not Unit.isExist(theUnit) @@ -984,18 +934,15 @@ function cfxPlayerScore.scheduledAward(args) trigger.action.outText("Player <" .. playerName .. "> lost score.", 30) return end - local uid = theUnit:getID() if theUnit:inAir() then trigger.action.outTextForUnit(uid, "Can't award score to <" .. playerName .. ">: unit not on the ground.", 30) return end - if theUnit:getLife() < 1 then trigger.action.outTextForUnit(uid, "Can't award score to <" .. playerName .. ">: unit did not survive landing.", 30) return -- needs to reslot, don't have to nil player score end - -- see if player is *still* within a scoreSafe zone local loc = theUnit:getPoint() local coa = theUnit:getCoalition() @@ -1008,12 +955,10 @@ function cfxPlayerScore.scheduledAward(args) end end end - if not isSafe then trigger.action.outTextForUnit(uid, "Can't award score for <" .. playerName .. ">, not in safe zone.", 30) return end - local theScore = cfxPlayerScore.getPlayerScore(playerName) local playerSide = dcsCommon.playerName2Coalition(playerName) if playerSide < 1 then @@ -1023,14 +968,12 @@ function cfxPlayerScore.scheduledAward(args) if dcsCommon.getSizeOfTable(theScore.killQueue) < 1 and dcsCommon.getSizeOfTable(theScore.featQueue) < 1 and theScore.scoreaccu < 1 then - -- player changed planes or - -- there was nothing to award + -- player changed planes or there was nothing to award trigger.action.outTextForUnit(uid, "Thank you, " .. playerName .. ", no scores or feats pending.", 30) return end local hasAward = false - -- when we get here we award all scores, kills, and feats local desc = "\nPlayer " .. playerName .. " is awarded:\n" -- score and total score @@ -1044,8 +987,7 @@ function cfxPlayerScore.scheduledAward(args) end theScore.scoreaccu = 0 hasAward = true - end - + end if cfxPlayerScore.verbose then trigger.action.outText("Iterating kill q <" .. dcsCommon.getSizeOfTable(theScore.killQueue) .. "> and feat q <" .. dcsCommon.getSizeOfTable(theScore.featQueue) .. ">", 30) end @@ -1059,7 +1001,6 @@ function cfxPlayerScore.scheduledAward(args) hasAward = true end theScore.killQueue = {} - -- iterate feats if dcsCommon.getSizeOfTable(theScore.featQueue) > 0 then desc = desc .. " confirmed feats:\n" @@ -1070,11 +1011,9 @@ function cfxPlayerScore.scheduledAward(args) hasAward = true end theScore.featQueue = {} - if cfxPlayerScore.reportCoalition then desc = desc .. "\nCoalition Total: " .. cfxPlayerScore.coalitionScore[playerSide] end - -- output score desc = desc .. "\n" if hasAward then @@ -1089,11 +1028,9 @@ function cfxPlayerScore.handlePlayerDeath(theEvent) -- only counts once local theUnit = theEvent.initiator local uName = theUnit:getName() - if cfxPlayerScore.verbose then trigger.action.outText("+++pScr: LOA/player death handler entry for <" .. uName .. ">", 30) end - local pName = cfxPlayerScore.unit2player[uName] if pName then -- this was a player name with link still live. @@ -1113,14 +1050,12 @@ function cfxPlayerScore.handlePlayerDeath(theEvent) trigger.action.outText("+++pScr - no action for LOA", 30) end end - end function cfxPlayerScore.handlePlayerEvent(theEvent) if theEvent.id == 28 then -- kill from player detected. cfxPlayerScore.killDetected(theEvent) - elseif theEvent.id == 15 then -- birth -- access player score for player. this will -- allocate if doesn't exist. Any player ever @@ -1130,7 +1065,6 @@ function cfxPlayerScore.handlePlayerEvent(theEvent) local playerName = thePlayerUnit:getPlayerName() local theScore = cfxPlayerScore.getPlayerScore(playerName) -- now re-init feat and score queues - if theScore.scoreaccu and theScore.scoreaccu > 0 then trigger.action.outTextForCoalition(playerSide, "Player " .. playerName .. ", score of <" .. theScore.scoreaccu .. "> points discarded.", 30) end @@ -1146,9 +1080,10 @@ function cfxPlayerScore.handlePlayerEvent(theEvent) -- write back cfxPlayerScore.setPlayerScore(playerName, theScore) - elseif theEvent.id == 4 then -- land + elseif theEvent.id == 4 or theEvent.id == 55 then -- land -- see if plane is still connected to player local theUnit = theEvent.initiator + if not theUnit.getName then return end -- dcs oddity precaution local uName = theUnit:getName() if cfxPlayerScore.unit2player[uName] then -- is filtered if too soon after last take-off/landing @@ -1158,7 +1093,6 @@ function cfxPlayerScore.handlePlayerEvent(theEvent) trigger.action.outText("+++pScr: filtered landing for <" .. uName .. ">: player no longer linked to unit", 30) end end - end end @@ -1167,69 +1101,51 @@ function cfxPlayerScore.readConfigZone(theZone) -- default scores cfxPlayerScore.aircraft = theZone:getNumberFromZoneProperty("aircraft", 50) cfxPlayerScore.helo = theZone:getNumberFromZoneProperty("helo", 40) - cfxPlayerScore.ground = theZone:getNumberFromZoneProperty("ground", 10) + cfxPlayerScore.ground = theZone:getNumberFromZoneProperty("ground", 10) cfxPlayerScore.ship = theZone:getNumberFromZoneProperty("ship", 80) cfxPlayerScore.train = theZone:getNumberFromZoneProperty( "train", 5) cfxPlayerScore.landing = theZone:getNumberFromZoneProperty("landing", 0) -- if > 0 then feat - cfxPlayerScore.pkMod = theZone:getNumberFromZoneProperty( "pkMod", 1) -- factor for killing a player cfxPlayerScore.ffMod = theZone:getNumberFromZoneProperty( "ffMod", -2) -- factor for friendly fire cfxPlayerScore.planeLoss = theZone:getNumberFromZoneProperty("planeLoss", -10) -- points added when player's plane crashes - cfxPlayerScore.announcer = theZone:getBoolFromZoneProperty("announcer", true) - if theZone:hasProperty("badSound") then cfxPlayerScore.badSound = theZone:getStringFromZoneProperty("badSound", "") end if theZone:hasProperty("scoreSound") then cfxPlayerScore.scoreSound = theZone:getStringFromZoneProperty("scoreSound", "") end - -- triggering saving scores if theZone:hasProperty("saveScore?") then cfxPlayerScore.saveScore = theZone:getStringFromZoneProperty("saveScore?", "none") cfxPlayerScore.lastSaveScore = trigger.misc.getUserFlag(cfxPlayerScore.saveScore) cfxPlayerScore.incremental = theZone:getBoolFromZoneProperty("incremental", false) -- incremental saves end - -- triggering show all scores if theZone:hasProperty("showScore?") then cfxPlayerScore.showScore = theZone:getStringFromZoneProperty("showScore?", "none") cfxPlayerScore.lastShowScore = trigger.misc.getUserFlag(cfxPlayerScore.showScore) end - cfxPlayerScore.rankPlayers = theZone:getBoolFromZoneProperty("rankPlayers", false) - cfxPlayerScore.scoreOnly = theZone:getBoolFromZoneProperty("scoreOnly", true) - cfxPlayerScore.deferred = theZone:getBoolFromZoneProperty("deferred", false) - cfxPlayerScore.delayAfterLanding = theZone:getNumberFromZoneProperty("delayAfterLanding", 10) - cfxPlayerScore.scoreFileName = theZone:getStringFromZoneProperty("scoreFileName", "Player Scores") - cfxPlayerScore.reportScore = theZone:getBoolFromZoneProperty("reportScore", true) - cfxPlayerScore.reportFeats = theZone:getBoolFromZoneProperty("reportFeats", true) - cfxPlayerScore.reportCoalition = theZone:getBoolFromZoneProperty("reportCoalition", false) -- also show coalition score - cfxPlayerScore.noGrief = theZone:getBoolFromZoneProperty( "noGrief", true) -- noGrief = only add positive score - if theZone:hasProperty("redScore#") then cfxPlayerScore.redScoreOut = theZone:getStringFromZoneProperty("redScore#") theZone:setFlagValue(cfxPlayerScore.redScoreOut, cfxPlayerScore.coalitionScore[1]) end - if theZone:hasProperty("blueScore#") then cfxPlayerScore.blueScoreOut = theZone:getStringFromZoneProperty("blueScore#") theZone:setFlagValue(cfxPlayerScore.blueScoreOut, cfxPlayerScore.coalitionScore[2]) end - if theZone:hasProperty("sharedData") then cfxPlayerScore.sharedData = theZone:getStringFromZoneProperty("sharedData", "cfxNameMissing") end - cfxPlayerScore.score2finance = theZone:getNumberFromZoneProperty("score2finance", 1) -- factor to convert points to bank finance end @@ -1287,11 +1203,9 @@ function cfxPlayerScore.loadData() end end end - -- -- save scores (text file) -- - function cfxPlayerScore.saveScores(theText, name) if not _G["persistence"] then trigger.action.outText("+++pScr: persistence module required to save scores. Here are the scores that I would have saved to <" .. name .. ">:\n", 30) @@ -1331,7 +1245,6 @@ function cfxPlayerScore.saveScoreToFile() -- local built score table local ranked = cfxPlayerScore.rankPlayers local theText = cfxPlayerScore.scoreTextForAllPlayers(ranked) - -- save to disk cfxPlayerScore.saveScores(theText, cfxPlayerScore.scoreFileName) end @@ -1341,14 +1254,12 @@ function cfxPlayerScore.showScoreToAll() local theText = cfxPlayerScore.scoreTextForAllPlayers(ranked) trigger.action.outText(theText, 30) end - -- -- Update -- function cfxPlayerScore.update() -- re-invoke in 1 second timer.scheduleFunction(cfxPlayerScore.update, {}, timer.getTime() + 1) - -- see if someone banged on saveScore if cfxPlayerScore.saveScore then if cfxZones.testZoneFlag(cfxPlayerScore, cfxPlayerScore.saveScore, "change", "lastSaveScore") then @@ -1358,7 +1269,6 @@ function cfxPlayerScore.update() cfxPlayerScore.saveScoreToFile() end end - -- showScore perhaps? if cfxPlayerScore.showScore then if cfxZones.testZoneFlag(cfxPlayerScore, cfxPlayerScore.showScore, "change", "lastShowScore") then @@ -1368,7 +1278,6 @@ function cfxPlayerScore.update() cfxPlayerScore.showScoreToAll() end end - -- check score flags if cfxPlayerScore.blueTriggerFlags then local coa = 2 @@ -1377,13 +1286,11 @@ function cfxPlayerScore.update() if tVal ~= newVal then -- score! cfxPlayerScore.coalitionScore[coa] = cfxPlayerScore.coalitionScore[coa] + cfxPlayerScore.blueTriggerScore[tName] - cfxPlayerScore.blueTriggerFlags[tName] = newVal - + cfxPlayerScore.blueTriggerFlags[tName] = newVal if cfxPlayerScore.announcer then trigger.action.outTextForCoalition(coa, "BLUE goal [" .. tName .. "] achieved, new BLUE coalition score is " .. cfxPlayerScore.coalitionScore[coa], 30) trigger.action.outSoundForCoalition(coa, cfxPlayerScore.scoreSound) end - -- bank it if exists local amount if bank and bank.addFunds then @@ -1402,17 +1309,12 @@ function cfxPlayerScore.update() local newVal = trigger.misc.getUserFlag(tName) if tVal ~= newVal then -- score! - cfxPlayerScore.coalitionScore[coa] = cfxPlayerScore.coalitionScore[coa] + cfxPlayerScore.redTriggerScore[tName] cfxPlayerScore.redTriggerFlags[tName] = newVal - --if bank and bank.addFunds then - -- bank.addFunds(coa, cfxPlayerScore.score2finance * cfxPlayerScore.blueTriggerScore[tName]) - --end if cfxPlayerScore.announcer then trigger.action.outTextForCoalition(coa, "RED goal [" .. tName .. "] achieved, new RED coalition score is " .. cfxPlayerScore.coalitionScore[coa], 30) trigger.action.outSoundForCoalition(coa, cfxPlayerScore.scoreSound) end - -- bank it if exists local amount if bank and bank.addFunds then @@ -1429,7 +1331,6 @@ function cfxPlayerScore.update() if cfxPlayerScore.redScoreOut then cfxZones.setFlagValue(cfxPlayerScore.redScoreOut, cfxPlayerScore.coalitionScore[1], cfxPlayerScore) end - if cfxPlayerScore.blueScoreOut then cfxZones.setFlagValue(cfxPlayerScore.blueScoreOut, cfxPlayerScore.coalitionScore[2], cfxPlayerScore) end @@ -1448,19 +1349,8 @@ function cfxPlayerScore.start() -- identify and process a score table zones local theZone = cfxZones.getZoneByName("playerScoreTable") if theZone then --- trigger.action.outText("Reading custom player score table", 30) -- read all into my types registry, replacing whatever is there cfxPlayerScore.typeScore = theZone:getAllZoneProperties(true) -- true = get all properties in UPPER case --- local n = dcsCommon.getSizeOfTable(cfxPlayerScore.typeScore) --- trigger.action.outText("Table has <" .. n .. "> entries:", 30) - if true then - --trigger.action.outText("Custom PlayerScore Type Score Table:", 30) - for name, val in pairs (cfxPlayerScore.typeScore) do --- trigger.action.outText("ps[" .. name .. "]=<" .. val .. ">", 30) - end - end - else - --trigger.action.outText("No custom score defined", 30) end -- read score tiggers and values @@ -1493,43 +1383,37 @@ function cfxPlayerScore.start() end cfxPlayerScore.blueTriggerFlags[tName] = trigger.misc.getUserFlag(tName) end - end - + end -- now read my config zone local theZone = cfxZones.getZoneByName("playerScoreConfig") if not theZone then theZone = cfxZones.createSimpleZone("playerScoreConfig") end cfxPlayerScore.readConfigZone(theZone) - -- read all scoreSafe zones local safeZones = cfxZones.zonesWithProperty("scoreSafe") for k, aZone in pairs(safeZones) do cfxPlayerScore.addSafeZone(aZone) end - -- read all feat zones local featZones = cfxZones.zonesWithProperty("feat") for k, aZone in pairs(featZones) do cfxPlayerScore.addFeatZone(aZone) - end - + end -- read all kill zones local killZones = cfxZones.zonesWithProperty("killZone") for k, aZone in pairs(killZones) do cfxPlayerScore.addKillZone(aZone) end - -- check that deferred has scoreSafe zones if cfxPlayerScore.deferred and dcsCommon.getSizeOfTable(cfxPlayerScore.safeZones) < 1 then trigger.action.outText("+++pScr: WARNING - deferred scoring active but no 'scoreSafe' zones set!", 30) end - + -- subscribe to events and use dcsCommon's handler structure dcsCommon.addEventHandler(cfxPlayerScore.handlePlayerEvent, cfxPlayerScore.preProcessor, - cfxPlayerScore.postProcessor) - + cfxPlayerScore.postProcessor) -- now load all save data and populate map with troops that -- we deployed last save. if persistence then @@ -1543,7 +1427,6 @@ function cfxPlayerScore.start() -- start update cfxPlayerScore.update() - trigger.action.outText("cfxPlayerScore v" .. cfxPlayerScore.version .. " started", 30) return true end diff --git a/modules/playerScoreUI.lua b/modules/playerScoreUI.lua index 88103dc..4dc78d3 100644 --- a/modules/playerScoreUI.lua +++ b/modules/playerScoreUI.lua @@ -1,5 +1,5 @@ cfxPlayerScoreUI = {} -cfxPlayerScoreUI.version = "2.1.0" +cfxPlayerScoreUI.version = "3.0.0" cfxPlayerScoreUI.verbose = false --[[-- VERSION HISTORY @@ -11,6 +11,7 @@ cfxPlayerScoreUI.verbose = false - score summary for side - allowAll - 2.1.1 - minor cleanup + - 3.0.0 - compatible with dynamic groups/units in DCS 2.9.6 --]]-- cfxPlayerScoreUI.requiredLibs = { @@ -19,7 +20,7 @@ cfxPlayerScoreUI.requiredLibs = { "cfxPlayerScore", } cfxPlayerScoreUI.soundFile = "Quest Snare 3.wav" -cfxPlayerScoreUI.rootCommands = {} -- by unit's GROUP name, for player aircraft +cfxPlayerScoreUI.rootCommands = {} -- by unit's GROUP name, for player aircraft. stores command roots cfxPlayerScoreUI.allowAll = true cfxPlayerScoreUI.ranked = true diff --git a/modules/radioMenus.lua b/modules/radioMenus.lua index 40b054c..39e0a14 100644 --- a/modules/radioMenus.lua +++ b/modules/radioMenus.lua @@ -1,30 +1,23 @@ radioMenu = {} -radioMenu.version = "3.0.0" +radioMenu.version = "4.0.0" radioMenu.verbose = false radioMenu.ups = 1 radioMenu.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course } +-- note: cfxMX is optional unless using types or groups attributes radioMenu.menus = {} radioMenu.mainMenus = {} -- dict +radioMenu.lateGroups = {} -- dict by ID --[[-- Version History - 2.1.0 - valA/valB/valC/valD attributes - OOP cfxZones - corrected CD setting for "D" - ackA, ackB, ackC, ackD attributes - valA-D now define full method, not just values - full wildcard support for ack and cooldown - 2.1.1 - outMessage now works correctly - 2.2.0 - clean-up - 2.2.1 - corrected ackD - 2.3.0 - added wildcard "*" ability for group name match - - added ackASnd .. ackDSnd sounds as options 3.0.0 - new radioMainMenu and attachTo: mechanics cascading radioMainMenu support - detect cyclic references + detect cyclic references + 4.0.0 - added infrastructure for dynamic players (DCS 2.9.6) + - added dynamic player support --]]-- function radioMenu.addRadioMenu(theZone) @@ -53,15 +46,33 @@ end -- -- read zone -- +function radioMenu.lateFilterPlayerForType(theUnit, theZone) + -- returns true if theUnit matches zone's group criterium + -- false otherwise + if not theUnit then return false end + local theGroup = theUnit:getGroup() + local theCat = theGroup:getCategory() -- 0 == aircraft 1 = plance + local lateGroupName = theGroup:getName() + local lateType = theUnit:getTypeName() + local allTypes = theZone.menuTypes -- {} + for idx, aType in pairs(allTypes) do + local lowerType = string.lower(aType) + if dcsCommon.stringStartsWith(lowerType, "helo") or dcsCommon.stringStartsWith(lowerType, "heli") or + aType == "helicopter" then + if theCat == 1 then return true end + elseif lowerType == "plane" or lowerType == "planes" then + if theCat == 0 then return true end + else + if aType == lateType then return true end + end + end + return false +end + function radioMenu.filterPlayerIDForType(theZone) -- note: we currently ignore coalition local theIDs = {} - local allTypes = {} - if dcsCommon.containsString(theZone.menuTypes, ",") then - allTypes = dcsCommon.splitString(theZone.menuTypes, ",") - else - table.insert(allTypes, theZone.menuTypes) - end + local allTypes = theZone.menuTypes -- {} -- now iterate all types, and include any player that matches -- note that players may match twice, so we use a dict @@ -116,16 +127,47 @@ function radioMenu.filterPlayerIDForType(theZone) return theIDs end +function radioMenu.lateFilterPlayerForGroup(theUnit, theZone) + -- returns true if theUnit matches zone's group criterium + -- false otherwise. NO COA CHECK + if not theUnit then return false end + local theGroup = theUnit:getGroup() + local lateGroupName = theGroup:getName() + for idx, gName in pairs(theZone.menuGroup) do + if dcsCommon.stringEndsWith(gName, "*") then + -- we must check all group names if they start with the + -- the same root. WARNING: CASE-SENSITIVE!!!! + gName = dcsCommon.removeEnding(gName, "*") + if dcsCommon.stringStartsWith(lateGroupName, gName) then + -- group match, install menu + if theZone.verbose or radioMenu.verbose then + trigger.action.outText("+++menu: WILDCARD Player Group <" .. gName .. "*> matched with <" .. mxName .. ">: gID = <" .. gID .. ">", 30) + end + return true + end + else + if lateGroupName == gName then + if theZone.verbose or radioMenu.verbose then + trigger.action.outText("+++menu: Player Group <" .. gName .. "> found: <" .. gID .. ">", 30) + end + return true + end + end + end + return false +end + +function radioMenu.lateFilterCoaForUnit(theUnit, theZone) + if theZone.coalition == 0 then return true end + if theZone.coalition == theUnit:getCoalition() then return true end + return false +end + function radioMenu.filterPlayerIDForGroup(theZone) -- create an iterable list of groups, separated by commas -- note that we could introduce wildcards for groups later local theIDs = {} - local allGroups = {} - if dcsCommon.containsString(theZone.menuGroup, ",") then - allGroups = dcsCommon.splitString(theZone.menuGroup, ",") - else - table.insert(allGroups, theZone.menuGroup) - end + local allGroups = theZone.menuGroup for idx, gName in pairs(allGroups) do -- if gName ends in wildcard "*" we process differently @@ -162,6 +204,45 @@ function radioMenu.filterPlayerIDForGroup(theZone) return theIDs end +function radioMenu.lateInstallMenu(theUnit, theZone) + -- we only add the group-individual menus (type/group). + -- all higher-level menus have been installed already + local theGroup = theUnit:getGroup() + local grp = theGroup:getID() + local gName = theGroup:getName() + radioMenu.lateGroups[grp] = gName + if not theZone.rootMenu then theZone.rootMenu = {} end + if theZone.attachTo then + local mainMenu = theZone.attachTo + theZone.mainRoot = radioMenu.getMainMenuFor(mainMenu, theZone, grp) + end + if theZone.menuGroup or theZone.menuTypes then + -- install menu, drop through to below + else + --trigger.action.outText("late-skipped menu for <" .. theZone.name .. ">, no group or type dependency", 30) + return + end + local aRoot = missionCommands.addSubMenuForGroup(grp, theZone.rootName, theZone.mainRoot) + theZone.rootMenu[grp] = aRoot + theZone.mcdA[grp] = 0 + theZone.mcdB[grp] = 0 + theZone.mcdC[grp] = 0 + theZone.mcdD[grp] = 0 + if theZone.itemA then + theZone.menuA[grp] = missionCommands.addCommandForGroup(grp, theZone.itemA, theZone.rootMenu[grp], radioMenu.redirectMenuX, {theZone, "A", grp}) + end + if theZone.itemB then + theZone.menuB[grp] = missionCommands.addCommandForGroup(grp, theZone.itemB, theZone.rootMenu[grp], radioMenu.redirectMenuX, {theZone, "B", grp}) + end + if theZone.itemC then + theZone.menuC[grp] = missionCommands.addCommandForGroup(grp, theZone.itemC, theZone.rootMenu[grp], radioMenu.redirectMenuX, {theZone, "C", grp}) + end + if theZone.itemD then + theZone.menuD[grp] = missionCommands.addCommandForGroup(grp, theZone.itemD, theZone.rootMenu[grp], radioMenu.redirectMenuX, {theZone, "D", grp}) + end + --trigger.action.outText("completed late-add menu for <" .. theZone.name .. "> to <" .. theUnit:getName() .. ">", 30) +end + function radioMenu.installMenu(theZone) local gID = nil if theZone.menuGroup then @@ -220,8 +301,8 @@ function radioMenu.installMenu(theZone) theZone.rootMenu[0] = missionCommands.addSubMenuForCoalition(theZone.coalition, theZone.rootName, theZone.mainRoot) end - if theZone:hasProperty("itemA") then - local menuA = theZone:getStringFromZoneProperty("itemA", "") + if theZone.itemA then -- :hasProperty("itemA") then + local menuA = theZone.itemA -- theZone:getStringFromZoneProperty("itemA", "") if theZone.menuGroup or theZone.menuTypes then theZone.menuA = {} for idx, grp in pairs(gID) do @@ -234,8 +315,8 @@ function radioMenu.installMenu(theZone) end end - if theZone:hasProperty("itemB") then - local menuB = theZone:getStringFromZoneProperty("itemB", "") + if theZone.itemB then --:hasProperty("itemB") then + local menuB = theZone.itemB -- :getStringFromZoneProperty("itemB", "") if theZone.menuGroup or theZone.menuTypes then theZone.menuB = {} for idx, grp in pairs(gID) do @@ -248,8 +329,8 @@ function radioMenu.installMenu(theZone) end end - if theZone:hasProperty("itemC") then - local menuC = theZone:getStringFromZoneProperty("itemC", "") + if theZone.itemC then --:hasProperty("itemC") then + local menuC = theZone.itemC -- :getStringFromZoneProperty("itemC", "") if theZone.menuGroup or theZone.menuTypes then theZone.menuC = {} for idx, grp in pairs(gID) do @@ -262,8 +343,8 @@ function radioMenu.installMenu(theZone) end end - if theZone:hasProperty("itemD") then - local menuD = theZone:getStringFromZoneProperty("itemD", "") + if theZone.itemD then -- :hasProperty("itemD") then + local menuD = theZone.itemD -- :getStringFromZoneProperty("itemD", "") if theZone.menuGroup or theZone.menuTypes then theZone.menuD = {} for idx, grp in pairs(gID) do @@ -292,6 +373,20 @@ function radioMenu.createRadioMenuWithZone(theZone) end end + -- read items and their stuff + if theZone:hasProperty("itemA") then + theZone.itemA = theZone:getStringFromZoneProperty("itemA") + end + if theZone:hasProperty("itemB") then + theZone.itemB = theZone:getStringFromZoneProperty("itemB") + end + if theZone:hasProperty("itemC") then + theZone.itemC = theZone:getStringFromZoneProperty("itemC") + end + if theZone:hasProperty("itemD") then + theZone.itemD = theZone:getStringFromZoneProperty("itemD") + end + theZone.coalition = theZone:getCoalitionFromZoneProperty("coalition", 0) -- groups / types if theZone:hasProperty("group") then @@ -306,6 +401,27 @@ function radioMenu.createRadioMenuWithZone(theZone) theZone.menuTypes = theZone:getStringFromZoneProperty("types", "none") end + -- now process menugroups and create sets to improve later speed + if theZone.menuGroup then + if dcsCommon.containsString(theZone.menuGroup, ",") then + local allGroups = dcsCommon.splitString(theZone.menuGroup, ",") + allGroups = dcsCommon.trimArray(allGroups) + theZone.menuGroup = allGroups + else + theZone.menuGroup = {theZone.menuGroup} --table.insert(allGroups, theZone.menuGroup) + end + end + + if theZone.menuTypes then + if dcsCommon.containsString(theZone.menuTypes, ",") then + local allTypes = dcsCommon.splitString(theZone.menuTypes, ",") + allTypes = dcsCommon.trimArray(allTypes) + theZone.menuTypes = allTypes + else + theZone.menuTypes = {theZone.menuTypes} + end + end + theZone.menuVisible = theZone:getBoolFromZoneProperty("menuVisible", true) -- install menu if not hidden @@ -392,13 +508,25 @@ end function radioMenu.getMainMenuFor(mainMenu, theZone, idx) if not idx then idx = 0 end if not mainMenu.rootMenu[idx] then --- trigger.action.outText("main <" .. mainMenu.name .. "> for zone <" .. theZone.name .. ">: forcing idx to 0", 30) return mainMenu.rootMenu[0] end --- trigger.action.outText("good main <" .. mainMenu.name .. "> for zone <" .. theZone.name .. ">", 30) return mainMenu.rootMenu[idx] end +function radioMenu.lateInstallMainMenu(theUnit, theZone) + local theGroup = theUnit:getGroup() + local grp = theGroup:getID() + local mainRoot = nil + if not theZone.rootMenu then theZone.rootMenu = {} end + if theZone.attachTo then + local mainMenu = theZone.attachTo + mainRoot = radioMenu.getMainMenuFor(mainMenu, theZone, grp) + end + local aRoot = missionCommands.addSubMenuForGroup(grp, theZone.rootName, mainRoot) + theZone.rootMenu[grp] = aRoot + --trigger.action.outText("menu: late-attached main menu <" .. theZone.name .. "> for unit <" .. theUnit:getName() .. ">", 30) +end + function radioMenu.installMainMenu(theZone) local gID = nil -- set of all groups this menu applies to if theZone.menuGroup then @@ -490,6 +618,27 @@ function radioMenu.createRadioMainMenuWithZone(theZone) elseif theZone:hasProperty("types") then theZone.menuTypes = theZone:getStringFromZoneProperty("types", "none") end + + -- now process menugroups and create sets to improve later speed + if theZone.menuGroup then + if dcsCommon.containsString(theZone.menuGroup, ",") then + local allGroups = dcsCommon.splitString(theZone.menuGroup, ",") + allGroups = dcsCommon.trimArray(allGroups) + theZone.menuGroup = allGroups + else + theZone.menuGroup = {theZone.menuGroup} --table.insert(allGroups, theZone.menuGroup) + end + end + + if theZone.menuTypes then + if dcsCommon.containsString(theZone.menuTypes, ",") then + local allTypes = dcsCommon.splitString(theZone.menuTypes, ",") + allTypes = dcsCommon.trimArray(allTypes) + theZone.menuTypes = allTypes + else + theZone.menuTypes = {theZone.menuTypes} + end + end -- always install this one radioMenu.installMainMenu(theZone) @@ -523,6 +672,8 @@ function radioMenu.radioOutMsg(ack, gid, theZone) local theMsg = ack if (gid > 0) and cfxMX then local gName = cfxMX.groupNamesByID[gid] + if not gName then gName = radioMenu.lateGroups[gid] end + if not gName then gName = "?*?*?" end theMsg = theMsg:gsub("", gName) end @@ -566,9 +717,9 @@ function radioMenu.setCDByGID(cd, theZone, gID, newVal) end function radioMenu.doMenuX(args) - theZone = args[1] - theItemIndex = args[2] -- A, B , C .. ? - theGroup = args[3] -- can be nil or groupID + local theZone = args[1] + local theItemIndex = args[2] -- A, B , C .. ? + local theGroup = args[3] -- can be nil or groupID if not theGroup then theGroup = 0 end local cd = radioMenu.cdByGID(theZone.mcdA, theZone, theGroup) --theZone.mcdA @@ -688,6 +839,74 @@ function radioMenu.update() end end +-- +-- onEvent - late dynamic spawns +-- +function radioMenu.lateMenuAddForUnit(theUnit) + -- iterate all menu zones and determine if this menu is to + -- install + local uName = theUnit:getName() + for zName, theZone in pairs(radioMenu.mainMenus) do + -- determine if this applies to us, and if so, install + -- for unit. only do this if menuGroups or menuTypes present + -- all others are set on coa level or higher + --trigger.action.outText("late-proccing MAIN menu <" .. zName ..">", 30) + if theZone.menuGroup then + if radioMenu.lateFilterCoaForUnit(theUnit, theZone) and + radioMenu.lateFilterPlayerForGroup(theUnit, theZone) then + radioMenu.lateInstallMainMenu(theUnit, theZone) + else + --trigger.action.outText("menu: skipped GROUP main menu <" .. zName .. "> add for <" .. uName .. ">", 30) + end + elseif theZone.menuTypes then + if radioMenu.lateFilterCoaForUnit(theUnit, theZone) and + radioMenu.lateFilterPlayerForType(theUnit, theZone) then + radioMenu.lateInstallMainMenu(theUnit, theZone) + else + --trigger.action.outText("menu: skipped TYPE main menu <" .. zName .. "> add for <" .. uName .. ">", 30) + end + else + -- nothing to do, was attached for coalition or higher + --trigger.action.outText("menu: did not late-install main menu <" .. zName .. ">, no group or type restrictions", 30) + end + end + + for idx, theZone in pairs(radioMenu.menus) do + -- again, does this apply to us? + --trigger.action.outText("late-proccing menu <" .. theZone.name ..">", 30) + if theZone.menuGroup then + if radioMenu.lateFilterCoaForUnit(theUnit, theZone) and + radioMenu.lateFilterPlayerForGroup(theUnit, theZone) then + radioMenu.lateInstallMenu(theUnit, theZone) + else + --trigger.action.outText("menu: skipped GROUP main menu <" .. theZone.name .. "> add for <" .. uName .. ">", 30) + end + elseif theZone.menuTypes then + if radioMenu.lateFilterCoaForUnit(theUnit, theZone) and + radioMenu.lateFilterPlayerForType(theUnit, theZone) then + radioMenu.lateInstallMenu(theUnit, theZone) + else + --trigger.action.outText("menu: skipped TYPE main menu <" .. theZone.name .. "> add for <" .. uName .. ">", 30) + end + else + -- nothing to do, was attached for coalition or higher + --trigger.action.outText("menu: did not late-install STD menu <" .. theZone.name .. ">, no group or type restrictions", 30) + end + end +end + + +function radioMenu:onEvent(event) + if not event then return end + if not event.initiator then return end + local theUnit = event.initiator + if event.id == 15 then + if not cfxMX.isDynamicPlayer(theUnit) then return end + -- we have a dynamic unit spawn + --trigger.action.outText("menu: detected dynamic spawn <" .. theUnit:getName() .. ">", 30) + radioMenu.lateMenuAddForUnit(theUnit) + end +end -- -- Config & Start @@ -756,6 +975,9 @@ function radioMenu.start() radioMenu.addRadioMenu(aZone) -- add to list end + -- install late spawn detector + world.addEventHandler(radioMenu) + -- start update radioMenu.update() diff --git a/modules/reaper.lua b/modules/reaper.lua index 8a00c63..b60d97f 100644 --- a/modules/reaper.lua +++ b/modules/reaper.lua @@ -1,5 +1,5 @@ reaper = {} -reaper.version = "1.1.0" +reaper.version = "1.2.0" reaper.requiredLibs = { "dcsCommon", "cfxZones", @@ -22,6 +22,7 @@ VERSION HISTORY - added FAC task - split task generation from wp generation - updated reaper naming, uniqueNames attribute (undocumented) + 1.2.0 - support twn when present --]]-- @@ -290,12 +291,21 @@ function reaper.setTarget(theZone, theTarget, cycled) local lp = theTarget:getPoint() local lat, lon, alt = coord.LOtoLL(lp) lat, lon = dcsCommon.latLon2Text(lat, lon) + local twnLoc = "" + if twn and towns then + local name, data, dist = twn.closestTownTo(lp) + local mdist= dist * 0.539957 + dist = math.floor(dist/100) / 10 + mdist = math.floor(mdist/100) / 10 + local bear = dcsCommon.compassPositionOfARelativeToB(lp, data.p) + twnLoc = " (" ..dist .. "km/" .. mdist .."nm " .. bear .. " of " .. name .. ")" + end local theSpot = Spot.createLaser(theZone.theUav, {0, 2, 0}, lp, theZone.code) if theZone.doSmoke then trigger.action.smoke(lp , theZone.smokeColor ) end - trigger.action.outTextForCoalition(theZone.coa, "Drone <" .. theZone.name .. "> is tracking a <" .. theTarget:getTypeName() .. "> at " .. lat .. " " .. lon .. ", code " .. theZone.code, 30) + trigger.action.outTextForCoalition(theZone.coa, "Drone <" .. theZone.name .. "> is tracking a <" .. theTarget:getTypeName() .. "> at " .. lat .. " " .. lon .. twnLoc ..", code " .. theZone.code, 30) trigger.action.outSoundForCoalition(theZone.coa, reaper.actionSound) theZone.theTarget = theTarget if theZone.theSpot then @@ -599,14 +609,23 @@ function reaper.doDroneStatus(args) local lat, lon, alt = coord.LOtoLL(lp) lat, lon = dcsCommon.latLon2Text(lat, lon) local ut = theTarget:getTypeName() - msg = msg .. ut .. " at " .. lat .. ", " .. lon .. " code " .. theZone.code + local twnLoc = "" + if twn and towns then + local tname, data, dist = twn.closestTownTo(lp) + local mdist= dist * 0.539957 + dist = math.floor(dist/100) / 10 + mdist = math.floor(mdist/100) / 10 + local bear = dcsCommon.compassPositionOfARelativeToB(lp, data.p) + twnLoc = " (" ..dist .. "km/" .. mdist .."nm " .. bear .. " of " .. tname .. ") " + end + msg = msg .. ut .. " at " .. lat .. ", " .. lon .. twnLoc .. " code " .. theZone.code else msg = msg .. "" end done[name] = true end else - msg = msg .. "\n(No drones are tracking a target)\n" + msg = msg .. "\n(No drones are tracking targets)\n" end -- collect loitering drones @@ -661,7 +680,16 @@ function reaper.doSingleDroneStatus(theZone) local lat, lon, alt = coord.LOtoLL(lp) lat, lon = dcsCommon.latLon2Text(lat, lon) local ut = theTarget:getTypeName() - msg = msg .. ut .. " at " .. lat .. ", " .. lon .. " code " .. theZone.code + local twnLoc = "" + if twn and towns then + local tname, data, dist = twn.closestTownTo(lp) + local mdist= dist * 0.539957 + dist = math.floor(dist/100) / 10 + mdist = math.floor(mdist/100) / 10 + local bear = dcsCommon.compassPositionOfARelativeToB(lp, data.p) + twnLoc = " (" ..dist .. "km/" .. mdist .."nm " .. bear .. " of " .. tname .. ") " + end + msg = msg .. ut .. " at " .. lat .. ", " .. lon .. twnLoc .. " code " .. theZone.code -- now add full group intelligence local collector = {} @@ -886,7 +914,6 @@ function reaper.start() timer.scheduleFunction(reaper.update, {}, timer.getTime() + 1) -- schedule scan and track loops --- timer.scheduleFunction(reaper.scan, {}, timer.getTime() + 1) timer.scheduleFunction(reaper.scanALT, {}, timer.getTime() + 1) timer.scheduleFunction(reaper.track, {}, timer.getTime() + 1) trigger.action.outText("reaper v " .. reaper.version .. " running.", 30) diff --git a/modules/reconGUI.lua b/modules/reconGUI.lua index d915d38..c4c0afc 100644 --- a/modules/reconGUI.lua +++ b/modules/reconGUI.lua @@ -1,16 +1,18 @@ cfxReconGUI = {} -cfxReconGUI.version = "1.0.0" +cfxReconGUI.version = "2.0.0" --[[-- VERSION HISTORY - 1.0.0 - initial version - + - 2.0.0 - removed dependence on cfxPlayer + - compatible with dynamically spawning players + - cleanup --]]-- --- find & command cfxGroundTroops-based jtacs --- UI installed via OTHER for all groups with players --- module based on xxxGrpUI - + cfxReconGUI.groupConfig = {} -- all inited group private config data cfxReconGUI.simpleCommands = true -- if true, f10 other invokes directly - +cfxReconGUI.requiredLibs = { + "dcsCommon", -- always + "cfxZones", -- Zones, of course +} -- -- C O N F I G H A N D L I N G -- ============================= @@ -38,7 +40,6 @@ function cfxReconGUI.createDefaultConfig(theGroup) local groupUnits = theGroup:getUnits() conf.unit = groupUnits[1] -- WARNING: ASSUMES ONE-UNIT GROUPS cfxReconGUI.resetConfig(conf) - conf.mainMenu = nil; -- this is where we store the main menu if we branch conf.myCommands = nil; -- this is where we store the commands if we branch @@ -80,12 +81,10 @@ function cfxReconGUI.getConfigForUnit(theUnit) return conf end --- -- -- M E N U H A N D L I N G --- ========================= --- -- + function cfxReconGUI.clearCommsSubmenus(conf) if conf.myCommands then for i=1, #conf.myCommands do @@ -96,16 +95,14 @@ end end function cfxReconGUI.removeCommsFromConfig(conf) - cfxReconGUI.clearCommsSubmenus(conf) - + cfxReconGUI.clearCommsSubmenus(conf) if conf.myMainMenu then missionCommands.removeItemForGroup(conf.id, conf.myMainMenu) conf.myMainMenu = nil end end --- this only works in single-unit groups. may want to check if group --- has disappeared +-- this only works in single-unit groups function cfxReconGUI.removeCommsForUnit(theUnit) if not theUnit then return end if not theUnit:isExist() then return end @@ -121,9 +118,6 @@ function cfxReconGUI.removeCommsForGroup(theGroup) cfxReconGUI.removeCommsFromConfig(conf) end --- --- set main root in F10 Other. All sub menus click into this --- function cfxReconGUI.isEligibleForMenu(theGroup) return true end @@ -144,7 +138,7 @@ end function cfxReconGUI.setCommsMenu(theGroup) -- depending on own load state, we set the command structure - -- it begins at 10-other, and has 'jtac' as main menu with submenus + -- it begins at F10-Other, and has 'Recon' as main menu with submenus -- as required if not theGroup then return end if not theGroup:isExist() then return end @@ -154,8 +148,7 @@ function cfxReconGUI.setCommsMenu(theGroup) if not cfxReconGUI.isEligibleForMenu(theGroup) then return end local conf = cfxReconGUI.getConfigForGroup(theGroup) - conf.id = theGroup:getID(); -- we do this ALWAYS so it is current even after a crash --- trigger.action.outText("+++ setting group <".. conf.theGroup:getName() .. "> jtac command", 30) + conf.id = theGroup:getID(); -- we ALWAYSdo this if cfxReconGUI.simpleCommands then -- we install directly in F-10 other @@ -190,26 +183,17 @@ function cfxReconGUI.setCommsMenu(theGroup) return end - - -- ok, first, if we don't have an F-10 menu, create one + -- if we don't have an F-10 menu, create one if not (conf.myMainMenu) then conf.myMainMenu = missionCommands.addSubMenuForGroup(conf.id, 'Recon') end - -- clear out existing commands cfxReconGUI.clearCommsSubmenus(conf) - - -- now we have a menu without submenus. -- add our own submenus - cfxReconGUI.addSubMenus(conf) - + cfxReconGUI.addSubMenus(conf) end function cfxReconGUI.addSubMenus(conf) - -- add menu items to choose from after - -- user clickedf on MAIN MENU. In this implementation - -- they all result invoked methods - local commandTxt = "Recon" local unitName = "bogus" if conf.unit and conf.unit:getName()then @@ -219,32 +203,10 @@ function cfxReconGUI.addSubMenus(conf) commandTxt = commandTxt .. "***" end - local theCommand = missionCommands.addCommandForGroup( - conf.id, - commandTxt, - conf.myMainMenu, - cfxReconGUI.redirectCommandX, - {conf, "recon", unitName} - ) + local theCommand = missionCommands.addCommandForGroup(conf.id, commandTxt, conf.myMainMenu, cfxReconGUI.redirectCommandX, {conf, "recon", unitName}) table.insert(conf.myCommands, theCommand) ---[[-- - commandTxt = "This is another important command" - theCommand = missionCommands.addCommandForGroup( - conf.id, - commandTxt, - conf.myMainMenu, - cfxReconGUI.redirectCommandX, - {conf, "Sub2"} - ) - table.insert(conf.myCommands, theCommand) ---]]-- end --- --- each menu item has a redirect and timed invoke to divorce from the --- no-debug zone in the menu invocation. Delay is .1 seconds --- - function cfxReconGUI.redirectCommandX(args) timer.scheduleFunction(cfxReconGUI.doCommandX, args, timer.getTime() + 0.1) end @@ -260,10 +222,8 @@ function cfxReconGUI.doCommandX(args) trigger.action.outText("+++ reconUI: doCommand: BOGUS unitName!", 30) end - local theGroup = conf.theGroup --- trigger.action.outTextForGroup(conf.id, "+++ groupUI: processing comms menu for <" .. what .. ">", 30) - - -- whenever we get here, we toggle the recon mode + local theGroup = conf.theGroup + -- when we get here, we toggle the recon mode local theUnit = conf.unit local message = "Scout ".. unitName .. " has stopped reporting." local theSide = conf.coalition @@ -285,94 +245,50 @@ function cfxReconGUI.doCommandX(args) end end trigger.action.outTextForCoalition(theSide, message, 30) - -- reset comms cfxReconGUI.removeCommsForGroup(theGroup) cfxReconGUI.setCommsMenu(theGroup) end - - --- --- G R O U P M A N A G E M E N T --- --- Group Management is required to make sure all groups --- receive a comms menu and that they receive a clean-up --- when required --- --- Callbacks are provided by cfxPlayer module to which we --- subscribe during init --- -function cfxReconGUI.playerChangeEvent(evType, description, player, data) - --trigger.action.outText("+++ groupUI: received <".. evType .. "> Event", 30) - if evType == "newGroup" then - -- initialized attributes are in data as follows - -- .group - new group - -- .name - new group's name - -- .primeUnit - the unit that trigggered new group appearing - -- .primeUnitName - name of prime unit - -- .id group ID - --theUnit = data.primeUnit - -- ensure group data exists and is updated - local conf = cfxReconGUI.getConfigForGroup(data.group) - conf.unit = data.primeUnit - conf.unitName = conf.unit:getName() -- will break if no exist - - cfxReconGUI.setCommsMenu(data.group) --- trigger.action.outText("+++ groupUI: added " .. theUnit:getName() .. " to comms menu", 30) - return +function cfxReconGUI:onEvent(theEvent) + if not theEvent then return end + if not theEvent.initiator then return end + local theUnit = theEvent.initiator + if not Unit.isExist(theUnit) then return end + if not theUnit.getName then return end + if not theUnit.getGroup then return end + if not theUnit.getPlayerName then return end + if not theUnit:getPlayerName() then return end + local theGroup = theUnit:getGroup() + if theEvent.id == 15 then + -- BIRTH EVENT PLAYER + local conf = cfxReconGUI.getConfigForGroup(theGroup) + conf.unit = theUnit --data.primeUnit + conf.unitName = theUnit:getName() + cfxReconGUI.setCommsMenu(theGroup) end - - if evType == "removeGroup" then - -- data is the player record that no longer exists. it consists of - -- .name - -- we must remove the comms menu for this group else we try to add another one to this group later - local conf = cfxReconGUI.getConfigByGroupName(data.name) - - if conf then - cfxReconGUI.removeCommsFromConfig(conf) -- remove menus - cfxReconGUI.resetConfig(conf) -- re-init this group for when it re-appears - else - trigger.action.outText("+++ reconUI: can't retrieve group <" .. data.name .. "> config: not found!", 30) - end - - return - end - - if evType == "leave" then - -- player unit left. we don't care since we only work on group level - -- if they were the only, this is followed up by group disappeared - - 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 - -- may need some logic to clean up old configs and/or menu items - - end - end -- -- Start -- function cfxReconGUI.start() - - -- iterate existing groups so we have a start situation - -- now iterate through all player groups and install the Assault Troop Menu - allPlayerGroups = cfxPlayerGroups -- cfxPlayerGroups is a global, don't fuck with it! - -- contains per group player record. Does not resolve on unit level! - for gname, pgroup in pairs(allPlayerGroups) do - local theUnit = pgroup.primeUnit -- get any unit of that group - cfxReconGUI.setCommsMenuForUnit(theUnit) -- set up + -- lib check + if not dcsCommon.libCheck("cfx Recon Mode", + cfxReconMode.requiredLibs) then + return false end - -- now install the new group notifier to install Assault Troops menu - cfxPlayer.addMonitor(cfxReconGUI.playerChangeEvent) + -- iterate existing groups so we have a start situation + local allPlayerUnits = dcsCommon.getAllExistingPlayerUnitsRaw() + for idx, theUnit in pairs(allPlayerUnits) do + cfxReconGUI.setCommsMenuForUnit(theUnit) + end + + world.addEventHandler(cfxReconGUI) + trigger.action.outText("cf/x cfxReconGUI v" .. cfxReconGUI.version .. " started", 30) - + return true end -- diff --git a/modules/reconMode.lua b/modules/reconMode.lua index 22845f3..62172d2 100644 --- a/modules/reconMode.lua +++ b/modules/reconMode.lua @@ -1,5 +1,5 @@ cfxReconMode = {} -cfxReconMode.version = "2.2.2" +cfxReconMode.version = "2.3.0" cfxReconMode.verbose = false -- set to true for debug info cfxReconMode.reconSound = "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" -- to be played when somethiong discovered @@ -60,7 +60,8 @@ VERSION HISTORY 2.2.1 - fixed "cfxReconSMode" typo 2.2.2 - added groupNames attribute - clean-up - + 2.3.0 - support for towns/twn when present + --]]-- cfxReconMode.detectionMinRange = 3000 -- meters at ground level @@ -394,6 +395,18 @@ function cfxReconMode.getLocation(theGroup) lat, lon = dcsCommon.latLon2Text(lat, lon) msg = "Lat " .. lat .. " Lon " .. lon .. " Ele " .. ele ..units end + + if twn and towns then + units = "km" + local village, data, dist = twn.closestTownTo(currPoint) + if cfxReconMode.imperialUnits then + dist = dist * 0.539957 -- nm conversion + units = "nm" + end + dist = math.floor(dist/100) / 10 + local bear = dcsCommon.compassPositionOfARelativeToB(currPoint, data.p) + msg = msg .. ", " .. dist .. units .. " " .. bear .. " of " .. village + end return msg end diff --git a/modules/scribe.lua b/modules/scribe.lua index 5cac9cd..762970d 100644 --- a/modules/scribe.lua +++ b/modules/scribe.lua @@ -1,5 +1,5 @@ scribe = {} -scribe.version = "2.0.0" +scribe.version = "2.0.2" scribe.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -13,10 +13,15 @@ VERSION HISTORY 1.0.1 postponed land, postponed takeoff, unit_lost 1.1.0 supports persistence's SHARED ability to share data across missions 2.0.0 support for main menu + 2.0.1 Hardening for DCS Jul 11 patch issues + 2.0.2 Secondary landing events correction + support for DCS dynamic player spawns + --]]-- scribe.verbose = true scribe.db = {} -- indexed by player name scribe.playerUnits = {} -- indexed by unit name. for crash detection +scribe.dynamicPlayers = {} --[[-- unitEntry: @@ -157,12 +162,22 @@ end -- Event handling -- function scribe.playerBirthedIn(playerName, theUnit) - -- access db - local theEntry = scribe.getPlayerNamed(playerName) -- can be new local myType = theUnit:getTypeName() local uName = theUnit:getName() local theGroup = theUnit:getGroup() local gID = theGroup:getID() + -- install menu if dynamic plane and not defined already + if cfxMX.isDynamicPlayer(theUnit) then + local gName = theGroup:getName() + if not scribe.dynamicPlayers[gName] then + scribe.installDynamicPlayerMenu(theUnit) + scribe.dynamicPlayers[gName] = true + end + end + + -- access db + local theEntry = scribe.getPlayerNamed(playerName) -- can be new + -- check if this player is still active if theEntry.isActive then -- do something to remedy this @@ -281,15 +296,15 @@ function scribe.playerLanded(playerName) -- see if last landing is at least xx seconds old local now = timer.getTime() delta = now - uEntry.lastLanding - if delta > scribe.landingCD or delta < 0 then + if delta > scribe.landingCD then -- or delta < 0 then uEntry.landings = uEntry.landings + 1 +-- trigger.action.outText("+++scrb: added landing for " .. playerName .. ", delta is <" .. delta .. ">.", 30) else if scribe.verbose then trigger.action.outText("+++scb: landing ignored: cooldown active", 30) end end uEntry.lastLanding = now - end function scribe.playerDeparted(playerName) @@ -342,6 +357,7 @@ function scribe:onEvent(theEvent) if not theEvent.initiator then return end local theUnit = theEvent.initiator if not theUnit then return end + if not theUnit.getName then return end -- DCS bug hardening local uName = theUnit:getName() if scribe.playerUnits[uName] and scribe.verbose then trigger.action.outText("+++scb: event <" .. theEvent.id .. " = " .. dcsCommon.event2text(theEvent.id) .. ">, concerns player unit named <" .. uName .. ">.", 30) @@ -362,10 +378,13 @@ function scribe:onEvent(theEvent) return end -- when we get here we have a player event - +-- trigger.action.outText("+++scrb: player event <" .. theEvent.id .. ">", 30) -- players can only ever activate by birth event - if theEvent.id == 15 then -- birth - scribe.playerBirthedIn(playerName, theUnit) + if theEvent.id == 15 + or theEvent == 20 + then -- birth / enter unit +-- trigger.action.outText("+++scrb: player <" .. playerName .. "> entered unit.", 30) + scribe.playerBirthedIn(playerName, theUnit) -- reset timer for landings / take-off scribe.playerUnits[uName] = playerName -- for crash helo detection end @@ -384,12 +403,12 @@ function scribe:onEvent(theEvent) end if theEvent.id == 4 or -- landed - theEvent.id == 56 then + theEvent.id == 55 then -- corrected to 55 scribe.playerLanded(playerName) end if theEvent.id == 3 or -- take-off - theEvent.id == 55 then -- postponed take-off + theEvent.id == 54 then -- postponed take-off, corrected to 54 scribe.playerDeparted(playerName) -- trigger.action.outText("departure detected", 30) end @@ -484,6 +503,31 @@ end -- -- start -- +function scribe.installDynamicPlayerMenu(theUnit) + local mainMenu = nil + if scribe.mainMenu then + mainMenu = radioMenu.getMainMenuFor(scribe.mainMenu) -- nilling both next params will return menus[0] + end + local unitInfo = {} + local theGroup = theUnit:getGroup() + local coa = theGroup:getCoalition() + local theType = theUnit:getTypeName() + local gName = theGroup:getName() + local uName = theUnit:getName() + if scribe.verbose then + trigger.action.outText("DYNAMIC unit <" .. uName .. ">: type <" .. theType .. "> coa <" .. coa .. ">, group <" .. gName .. ">", 30) + end + unitInfo.uName = uName -- needed for reverse-lookup + unitInfo.gName = gName -- also needed for reverse lookup + unitInfo.coa = coa + unitInfo.gID = theGroup:getID() + unitInfo.uID = theUnit:getID() + unitInfo.theType = theType +-- unitInfo.cat = cfxMX.groupTypeByName[gName] + unitInfo.root = missionCommands.addSubMenuForGroup(unitInfo.gID, scribe.uiMenu, mainMenu) + unitInfo.checkData = missionCommands.addCommandForGroup(unitInfo.gID, "Get Pilot's Statistics", unitInfo.root, scribe.redirectCheckData, unitInfo) +end + function scribe.startPlayerGUI() -- scan all mx players -- note: currently assumes single-player groups @@ -514,7 +558,7 @@ function scribe.startPlayerGUI() unitInfo.gID = gData.groupId unitInfo.uID = uData.unitId unitInfo.theType = theType - unitInfo.cat = cfxMX.groupTypeByName[gName] +-- unitInfo.cat = cfxMX.groupTypeByName[gName] unitInfo.root = missionCommands.addSubMenuForGroup(unitInfo.gID, scribe.uiMenu, mainMenu) unitInfo.checkData = missionCommands.addCommandForGroup(unitInfo.gID, "Get Pilot's Statistics", unitInfo.root, scribe.redirectCheckData, unitInfo) end diff --git a/modules/spawnZones.lua b/modules/spawnZones.lua index a770306..e97d513 100644 --- a/modules/spawnZones.lua +++ b/modules/spawnZones.lua @@ -1,5 +1,5 @@ cfxSpawnZones = {} -cfxSpawnZones.version = "2.0.3" +cfxSpawnZones.version = "2.1.0" cfxSpawnZones.requiredLibs = { "dcsCommon", -- common is of course needed for everything -- pretty stupid to check for this since we @@ -29,6 +29,8 @@ cfxSpawnZones.spawnedGroups = {} 2.0.1 - fix in verifySpawnOwnership() when not master zone found 2.0.2 - new "moveFormation" attribute 2.0.3 - corrected type in spawnUnits? attribute + 2.1.0 - masterOwner update for dmlZones. + since spawners don't extend zones, this is still old-school --]]-- @@ -111,7 +113,17 @@ function cfxSpawnZones.createSpawner(inZone) theSpawner.country = inZone:getNumberFromZoneProperty("country", 0) if inZone:hasProperty("masterOwner") then theSpawner.masterZoneName = inZone:getStringFromZoneProperty("masterOwner", "") - if theSpawner.masterZoneName == "" then theSpawner.masterZoneName = nil end + if theSpawner.masterZoneName == "" then + theSpawner.masterZoneName = nil + else + local masterZone = cfxZones.getZoneByName(theSpawner.masterZoneName) + if not masterZone then + trigger.action.outText("spawner " .. theSpawner.name .. " DID NOT FIND MASTER ZONE <" .. theSpawner.masterZoneName .. ">", 30) + theSpawner.masterZoneName = nil + else + theSpawner.masterZone = masterZone + end + end end theSpawner.rawOwner = coalition.getCountryCoalition(theSpawner.country) @@ -237,18 +249,18 @@ function cfxSpawnZones.verifySpawnOwnership(spawner) return true end -- no master owner, all ok local myCoalition = spawner.rawOwner - local masterZone = cfxZones.getZoneByName(spawner.masterZoneName) - if not masterZone then - trigger.action.outText("spawner " .. spawner.name .. " DID NOT FIND MASTER ZONE <" .. spawner.masterZoneName .. ">", 30) - return false - end - - if not masterZone.owner then +-- local masterZone = cfxZones.getZoneByName(spawner.masterZoneName) +-- if not masterZone then +-- trigger.action.outText("spawner " .. spawner.name .. " DID NOT FIND MASTER ZONE <" .. spawner.masterZoneName .. ">", 30) +-- return false +-- end + local masterZone = spawner.masterZone +-- if not masterZone.owner then --trigger.action.outText("spawner " .. spawner.name .. " - masterZone " .. masterZone.name .. " HAS NO OWNER????", 30) - return true - end +-- return true +-- end - if (myCoalition ~= masterZone.owner) then + if (myCoalition ~= masterZone:getCoalition()) then -- can't spawn, surrounding area owned by enemy return false end diff --git a/modules/stopGaps standalone.lua b/modules/stopGaps standalone.lua index 27e215a..5220788 100644 --- a/modules/stopGaps standalone.lua +++ b/modules/stopGaps standalone.lua @@ -1,5 +1,5 @@ stopGap = {} -stopGap.version = "1.1.1 STANDALONE" +stopGap.version = "1.2.0 STANDALONE" stopGap.verbose = false stopGap.ssbEnabled = true stopGap.ignoreMe = "-sg" @@ -8,6 +8,7 @@ stopGap.isMP = false stopGap.running = true stopGap.refreshInterval = -1 -- seconds to refresh all statics. -1 = never, 3600 = once every hour stopGap.kickTheDead = true -- kick players to spectators on death to prevent re-entry issues +stopGap.allNeutral = false -- make all stand-ins neutral --[[-- Written and (c) 2023 by Christian Franz @@ -37,6 +38,8 @@ stopGap.kickTheDead = true -- kick players to spectators on death to prevent re- 1.0.9 - optimization when turning on stopgap 1.1.0 - kickTheDead option 1.1.1 - filter "from runway" clients + 1.2.0 - compatibility with DCS dynamic spawns + --]]-- @@ -115,8 +118,12 @@ function stopGap.staticMXFromUnitMX(theGroup, theUnit) theStatic.heading = theUnit.heading -- may need some attention theStatic.type = theUnit.type theStatic.name = theUnit.name -- will magically be replaced with player unit + theStatic.payload = theUnit.payload -- not supported (yet) by DCS + theStatic.onboard_num = theUnit.onboard_num -- not supported theStatic.cty = cfxMX.countryByName[theGroup.name] - + if stopGap.allNeutral then + theStatic.cty = 82 -- UN Peache keepers, assign to neutral + end return theStatic end diff --git a/modules/stopGaps.lua b/modules/stopGaps.lua index e819fd6..bad3ca4 100644 --- a/modules/stopGaps.lua +++ b/modules/stopGaps.lua @@ -1,5 +1,5 @@ stopGap = {} -stopGap.version = "1.1.2" +stopGap.version = "1.2.0" stopGap.verbose = false stopGap.ssbEnabled = true stopGap.ignoreMe = "-sg" @@ -52,6 +52,9 @@ stopGap.requiredLibs = { 1.1.0 - kickTheDead option 1.1.1 - filter "from runway" clients 1.1.2 - allNeutral (DML only) + 1.2.0 - DCS dynamic player spawn compatibility + stopGaps only works with MX data, so we are good, no changes + required --]]-- @@ -77,6 +80,8 @@ function stopGap.staticMXFromUnitMX(theGroup, theUnit) theStatic.type = theUnit.type theStatic.name = theUnit.name -- will magically be replaced with player unit theStatic.cty = cfxMX.countryByName[theGroup.name] + theStatic.payload = theUnit.payload -- not supported (yet) by DCS + theStatic.onboard_num = theUnit.onboard_num -- not supported -- DML only: allNeutral if stopGap.allNeutral then theStatic.cty = dcsCommon.getACountryForCoalition(0) @@ -499,6 +504,4 @@ if not stopGap.start() then trigger.action.outText("+++ aborted stopGap v" .. stopGap.version .. " -- startup failed", 30) stopGap = nil end ---[[-- TODO - - allNeutral: spawn all player aircraft as neutral ---]]-- + diff --git a/modules/twn.lua b/modules/twn.lua new file mode 100644 index 0000000..383c90b --- /dev/null +++ b/modules/twn.lua @@ -0,0 +1,76 @@ +twn = {} +twn.version = "1.0.1" +twn.verbose = false + +--[[-- + A DML unicorn - doesn't require any other scripts to function + (C) 2024 by Christian Franz + +VERSION HISTORY +1.0.0 - Initial version +1.0.1 - Sinai // SinaiMap switcharoo + +--]]-- + +function twn.closestTownTo(p) -- returns name, data, distance + if not towns then + trigger.action.outText("+++twn: Towns undefined", 30) + return nil, nil, nil + end + local closest = nil + local theName = nil + local smallest = math.huge + local x = p.x + local z = p.z + for name, entry in pairs(towns) do + local dx = x - entry.p.x + local dz = z - entry.p.z + local d = dx * dx + dz * dz -- no need to take square root + if d < smallest then + smallest = d + closest = entry + theName = name + end + end + return theName, closest, smallest^0.5 -- root it! +end + +function twn.start() + -- get my theater + local theater = env.mission.theatre + -- map naming oddities + if theater == "SinaiMap" then theater = "Sinai" end + if twn.verbose then + trigger.action.outText("theater is <" .. theater .. ">", 30) + end + local path = "./Mods/terrains/" .. theater .. "/map/towns.lua" + -- assemble command + local command = 't = loadfile("' .. path .. '"); if t then t(); return net.lua2json(towns); else return nil end' + if twn.verbose then + trigger.action.outText("will run command <" .. command .. ">", 30) + end + local json = net.dostring_in("gui", command) + + if json then + towns = {} + traw = net.json2lua(json) + local count = 0 + for name, entry in pairs (traw) do + local p = coord.LLtoLO(entry.latitude, entry.longitude,0) + entry.p = p + towns[name] = entry + count = count + 1 + end + if twn.verbose then + trigger.action.outText("+++twn: <" .. count .. "> town records processed", 30) + end + else + trigger.action.outText("+++twn: no towns accessible.", 30) + return false + end + + trigger.action.outText("twn (towns importer) v " .. twn.version .. " started.", 30) + return true +end + +twn.start() diff --git a/modules/valet.lua b/modules/valet.lua index f4f2fea..28b3a7d 100644 --- a/modules/valet.lua +++ b/modules/valet.lua @@ -1,5 +1,5 @@ valet = {} -valet.version = "1.1.0" +valet.version = "1.1.1" valet.verbose = false valet.requiredLibs = { "dcsCommon", -- always @@ -14,6 +14,7 @@ valet.valets = {} 1.0.2 - also scan birth events 1.0.3 - outSoundFile now working correctly 1.1.0 - hysteresis is now time-based (10 seconds) + 1.1.1 - hardening against DCS July-11 update issues --]]-- function valet.addValet(theZone) @@ -361,6 +362,7 @@ function valet.checkPlayerSpawn(playerName, theUnit) -- see if player spawned in a valet zone if not playerName then return end if not theUnit then return end + if not theUnit.getName then return end local pos = theUnit:getPoint() --trigger.action.outText("+++valet: spawn event", 30) diff --git a/modules/williePete.lua b/modules/williePete.lua index 27e300d..ea4072e 100644 --- a/modules/williePete.lua +++ b/modules/williePete.lua @@ -1,5 +1,5 @@ williePete = {} -williePete.version = "2.0.5" +williePete.version = "2.1.0" williePete.ups = 10 -- we update at 10 fps, so accuracy of a -- missile moving at Mach 2 is within 33 meters, -- with interpolation even at 3 meters @@ -22,6 +22,8 @@ williePete.requiredLibs = { 2.0.3 - further hardened playerUpdate() 2.0.4 - support for the Kiowa's Hydra M259 2.0.5 - support for Mirage F1 WP that differ from Gazelle (?) + 2.0.6 - DCS Update 7-11 2024 weapon name bug + 2.1.0 - DCS update 2.9.6 dynamic spawn support --]]-- williePete.willies = {} @@ -134,6 +136,53 @@ end -- -- PLAYER MANAGEMENT -- +function williePete.latePlayerGUI(theUnit) + local unitInfo = {} + unitInfo.name = theUnit:getName() -- needed for reverse-lookup + local theGroup = theUnit:getGroup() + unitInfo.gName = theGroup:getName() -- gName -- also needed for reverse lookup + unitInfo.coa = theGroup:getCoalition() -- coa + unitInfo.gID = theGroup:getID() -- gData.groupId + unitInfo.uID = theUnit:getID() -- uData.unitId + unitInfo.theType = theUnit:getTypeName() -- theType + cat = theGroup:getCategory() + if cat == 0 then unitInfo.cat = "plane" + elseif cat == 1 then unitInfo.cat = "helicopter" + else + return -- whatever player is controlling, it's not for WP + end + + williePete.doGUIforUnitInfo(unitInfo) +end + +function williePete.doGUIforUnitInfo(unitInfo) + local pass = false + local uName = unitInfo.name + local gName = unitInfo.gName + + for idx, aType in pairs(williePete.facTypes) do + if aType == "ALL" then pass = true end + if aType == "ANY" then pass = true end + if aType == theType then pass = true end + if dcsCommon.stringStartsWith(aType, "HEL") and unitInfo.cat == "helicopter" then pass = true end + if dcsCommon.stringStartsWith(aType, "PLAN") and unitInfo.cat == "plane" then pass = true end + end + + if pass then -- we install a menu for this group + -- we may not want check in stuff, but it could be cool + if williePete.playerGUIs[gName] then + trigger.action.outText("+++WP: Warning: we already have WP menu for unit <" .. uName .. "> in group <" .. gName .. ">. Skipped.", 30) + elseif williePete.groupGUIs[gName] then + trigger.action.outText("+++WP: Warning: POSSIBLE MULTI-PLAYER UNIT GROUP DETECTED. We already have WP menu for Player Group <" .. gName .. ">. Skipped, only first unit supported. ", 30) + else + unitInfo.root = missionCommands.addSubMenuForGroup(unitInfo.gID, "FAC") + unitInfo.checkIn = missionCommands.addCommandForGroup(unitInfo.gID, "Check In", unitInfo.root, williePete.redirectCheckIn, unitInfo) + williePete.groupGUIs[gName] = unitInfo + williePete.playerGUIs[gName] = unitInfo + end + end +end + function williePete.startPlayerGUI() -- scan all mx players -- note: currently assumes single-player groups @@ -159,7 +208,7 @@ function williePete.startPlayerGUI() unitInfo.theType = theType unitInfo.cat = cfxMX.groupTypeByName[gName] -- now check type against willie pete config for allowable types - local pass = false +--[[-- local pass = false for idx, aType in pairs(williePete.facTypes) do if aType == "ALL" then pass = true end if aType == "ANY" then pass = true end @@ -181,7 +230,8 @@ function williePete.startPlayerGUI() williePete.playerGUIs[gName] = unitInfo end end - +--]]-- + williePete.doGUIforUnitInfo(unitInfo) -- store it - WARNING: ASSUMES SINGLE-UNIT Player Groups --williePete.playerGUIs[uName] = unitInfo end @@ -467,7 +517,7 @@ end function williePete.zedsDead(theObject) if not theObject then return end - + if not theObject.getName then return end -- DCS July-11 oddity. local theName = theObject:getName() -- now check if it's a registered blasted object:getSampleRate() -- in multi-unit player groups, this can can lead to @@ -493,6 +543,14 @@ function williePete:onEvent(event) return end + -- see if a dynamic spawn + if event.id == 15 then + local theUnit = event.initiator + if not cfxMX.isDynamicPlayer(theUnit) then return end + williePete.latePlayerGUI(theUnit) + return + end + -- check if it's a dead event if event.id == 8 then -- death event diff --git a/tutorial & demo missions/demo - bombs away.miz b/tutorial & demo missions/demo - bombs away.miz index b9c1c71..690ceb2 100644 Binary files a/tutorial & demo missions/demo - bombs away.miz and b/tutorial & demo missions/demo - bombs away.miz differ diff --git a/tutorial & demo missions/demo - escort a convoy to town.miz b/tutorial & demo missions/demo - escort a convoy to town.miz new file mode 100644 index 0000000..8f057d1 Binary files /dev/null and b/tutorial & demo missions/demo - escort a convoy to town.miz differ diff --git a/tutorial & demo missions/demo - reaper, man.miz b/tutorial & demo missions/demo - reaper, man.miz index 6450077..0f4e9a4 100644 Binary files a/tutorial & demo missions/demo - reaper, man.miz and b/tutorial & demo missions/demo - reaper, man.miz differ diff --git a/tutorial & demo missions/demo - recon mode - reloaded.miz b/tutorial & demo missions/demo - recon mode - reloaded.miz index d321f2a..d0753be 100644 Binary files a/tutorial & demo missions/demo - recon mode - reloaded.miz and b/tutorial & demo missions/demo - recon mode - reloaded.miz differ