diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 5054466..21e47c6 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 e7dea8e..f56d2a0 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 ae5b590..aa7af08 100644 --- a/modules/FARPZones.lua +++ b/modules/FARPZones.lua @@ -451,7 +451,7 @@ 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) + --trigger.action.outText("frpZ persistence: processing FARP <" .. fName .. ">", 30) local fData = {} fData.owner = theFARP.owner fData.defenderData = dcsCommon.clone(theFARP.defenderData) diff --git a/modules/RNDFlags.lua b/modules/RNDFlags.lua index 95f5dad..bc8c9a4 100644 --- a/modules/RNDFlags.lua +++ b/modules/RNDFlags.lua @@ -1,5 +1,5 @@ rndFlags = {} -rndFlags.version = "1.3.2" +rndFlags.version = "1.4.0" rndFlags.verbose = false rndFlags.requiredLibs = { "dcsCommon", -- always @@ -30,6 +30,8 @@ rndFlags.requiredLibs = { - added 'rndDone!' flag - rndMethod defaults to "inc" 1.3.2 - moved flagArrayFromString to dcsCommon + - minor clean-up + 1.4.0 - persistence --]] @@ -39,60 +41,16 @@ function rndFlags.addRNDZone(aZone) table.insert(rndFlags.rndGen, aZone) end +function rndFlags.getRNDByName(aName) + for idx, theRND in pairs(rndFlags.rndGen) do + if theRND.name == aName then return theRND end + end + return nil +end + function rndFlags.flagArrayFromString(inString) return dcsCommon.flagArrayFromString(inString, rndFlags.verbose) - --[[-- - if string.len(inString) < 1 then - trigger.action.outText("+++RND: empty flags", 30) - return {} - end - if rndFlags.verbose then - trigger.action.outText("+++RND: processing <" .. inString .. ">", 30) - end - local flags = {} - local rawElements = dcsCommon.splitString(inString, ",") - -- go over all elements - for idx, anElement in pairs(rawElements) do - if dcsCommon.stringStartsWithDigit(anElement) and dcsCommon.containsString(anElement, "-") then - -- interpret this as a range - local theRange = dcsCommon.splitString(anElement, "-") - local lowerBound = theRange[1] - lowerBound = tonumber(lowerBound) - local upperBound = theRange[2] - upperBound = tonumber(upperBound) - if lowerBound and upperBound then - -- swap if wrong order - if lowerBound > upperBound then - local temp = upperBound - upperBound = lowerBound - lowerBound = temp - end - -- now add add numbers to flags - for f=lowerBound, upperBound do - table.insert(flags, f) - - end - else - -- bounds illegal - trigger.action.outText("+++RND: ignored range <" .. anElement .. "> (range)", 30) - end - else - -- single number - f = dcsCommon.trim(anElement) -- DML flag upgrade: accept strings tonumber(anElement) - if f then - table.insert(flags, f) - - else - trigger.action.outText("+++RND: ignored element <" .. anElement .. "> (single)", 30) - end - end - end - if rndFlags.verbose then - trigger.action.outText("+++RND: <" .. #flags .. "> flags total", 30) - end - return flags - --]]-- end -- @@ -214,7 +172,6 @@ function rndFlags.fire(theZone) if rndFlags.verbose or theZone.verbose then trigger.action.outText("+++RND: RND " .. theZone.name .. " ran out of flags. Will fire 'done' instead ", 30) end - if theZone.doneFlag then cfxZones.pollFlag(theZone.doneFlag, theZone.rndMethod, theZone) end @@ -281,15 +238,67 @@ end function rndFlags.startCycle() for idx, theZone in pairs(rndFlags.rndGen) do if theZone.onStart then - if rndFlags.verbose or theZone.verbose then - trigger.action.outText("+++RND: starting " .. theZone.name, 30) + if theZone.isStarted then + -- suppressed by persistence + else + if rndFlags.verbose or theZone.verbose then + trigger.action.outText("+++RND: starting " .. theZone.name, 30) + end + rndFlags.fire(theZone) end - rndFlags.fire(theZone) end end end +-- +-- Load / Save data +-- +function rndFlags.saveData() + local theData = {} + local allRND = {} + for idx, theRND in pairs(rndFlags.rndGen) do + local theName = theRND.name + local rndData = {} + -- save data for this RND + rndData.myFlags = dcsCommon.clone(theRND.myFlags) + allRND[theName] = rndData + end + theData.allRND = allRND + return theData +end + +function rndFlags.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("rndFlags") + if not theData then + if rndFlags.verbose then + trigger.action.outText("+++RND Persistence: no save date received, skipping.", 30) + end + return + end + + local allRND = theData.allRND + if not allRND then + if rndFlags.verbose then + trigger.action.outText("+++RND Persistence - no data, skipping", 30) + end + return + end -- no data, no proccing + + for theName, rData in pairs(allRND) do + local theRND = rndFlags.getRNDByName(theName) + if theRND then + -- get current myFlags + local myFlags = dcsCommon.clone(rData.myFlags) + theRND.myFlags = myFlags + theRND.isStarted = true -- we are initted, NO ON START + else + trigger.action.outText("+++RND persistecne: can't synch RND <" .. theName .. ">", 30) + end + end +end + -- -- start module and read config -- @@ -312,7 +321,7 @@ end function rndFlags.start() -- lib check - if not dcsCommon.libCheck then + if not dcsCommon then trigger.action.outText("RNDFlags requires dcsCommon", 30) return false end @@ -345,6 +354,15 @@ function rndFlags.start() rndFlags.addRNDZone(aZone) -- remember it so we can smoke it end + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = rndFlags.saveData + persistence.registerModule("rndFlags", callbacks) + -- now load my data + rndFlags.loadData() + end + -- start cycle timer.scheduleFunction(rndFlags.startCycle, {}, timer.getTime() + 0.25) @@ -361,4 +379,3 @@ if not rndFlags.start() then rndFlags = nil end --- TODO: move flags to RND!, rename RND to RND!, deprecate flags! diff --git a/modules/cfxGroundTroops.lua b/modules/cfxGroundTroops.lua index 33a6564..552fe99 100644 --- a/modules/cfxGroundTroops.lua +++ b/modules/cfxGroundTroops.lua @@ -1,5 +1,5 @@ cfxGroundTroops = {} -cfxGroundTroops.version = "1.7.6" +cfxGroundTroops.version = "1.7.7" cfxGroundTroops.ups = 1 cfxGroundTroops.verbose = false cfxGroundTroops.requiredLibs = { @@ -61,6 +61,7 @@ cfxGroundTroops.deployedTroops = {} -- 1.7.4 - verbose flag, warnings suppressed -- 1.7.5 - some troop.group hardening with isExist() -- 1.7.6 - fixed switchToOffroad +-- 1.7.7 - no longer case sensitive for orders -- an entry into the deployed troop has the following attributes @@ -580,17 +581,17 @@ function cfxGroundTroops.updateTroops(troop) -- their order is removed, and the 'real' orders -- are revealed. For now, do nothing cfxGroundTroops.updateWait(troop) - + --REMEMBER: LOWER CASE ONLY! elseif troop.orders == "guard" then cfxGroundTroops.updateGuards(troop) - elseif troop.orders == "attackOwnedZone" then + elseif troop.orders == "attackownedzone" then cfxGroundTroops.updateZoneAttackers(troop) elseif troop.orders == "laze" then cfxGroundTroops.updateLaze(troop) - elseif troop.orders == "attackZone" then + elseif troop.orders == "attackzone" then cfxGroundTroops.updateAttackers(troop) else @@ -752,7 +753,7 @@ function cfxGroundTroops.updateSingleScheduled(params) -- check max speed of group. if < 0.1 then note and increase -- speedWarning. if not, reset speed warning - if troops.orders == "attackOwnedZone" and dcsCommon.getGroupMaxSpeed(troops.group) < 0.1 then + if troops.orders == "attackownedzone" and dcsCommon.getGroupMaxSpeed(troops.group) < 0.1 then if not troops.speedWarning then troops.speedWarning = 0 end troops.speedWarning = troops.speedWarning + 1 else @@ -918,7 +919,7 @@ function cfxGroundTroops.getTroopReport(theSide, ignoreInfantry) if troop.side == theSide and troop.group:isExist() then local unitNum = troop.group:getSize() report = report .. "\n" .. troop.name .. " (".. unitNum .."): <" .. troop.orders .. ">" - if troop.orders == "attackOwnedZone" then + if troop.orders == "attackownedzone" then if troop.destination then report = report .. " move towards " .. troop.destination.name else @@ -944,19 +945,16 @@ function cfxGroundTroops.createGroundTroops(inGroup, range, orders) local newTroops = {} if not orders then orders = "guard" - --trigger.action.outText("+++ adding ground troops <".. inGroup:getName() ..">with default orders", 30) - else - --trigger.action.outText("+++ adding ground troops <".. inGroup:getName() ..">with orders " .. orders, 30) end if orders:lower() == "lase" then - orders = "laze" -- we use WRONG spelling here, cause we're cool + orders = "laze" -- we use WRONG spelling here, cause we're cool. yeah, right. end newTroops.insideDestination = false newTroops.unscheduleCount = 0 -- will count up as we aren't scheduled newTroops.speedWarning = 0 newTroops.isOffroad = false -- if true, we switched to direct orders, not roads, after standstill newTroops.group = inGroup - newTroops.orders = orders + newTroops.orders = orders:lower() newTroops.coalition = inGroup:getCoalition() newTroops.side = newTroops.coalition -- because we'e been using both. newTroops.name = inGroup:getName() @@ -972,7 +970,8 @@ function cfxGroundTroops.addGroundTroopsToPool(troops) -- troops MUST be a table trigger.action.outText("+++ adding ground troops with unsupported troop signature", 30) return end - + if not troops.orders then troops.orders = "guard" end + troops.orders = troops.orders:lower() troops.reschedule = true -- in case we use scheduled update -- we now add to internal array. this is worked on by all -- update meths, on scheduled upadtes, it is only used to diff --git a/modules/cfxGroups.lua b/modules/cfxGroups.lua index 8ba0c15..0e741d1 100644 --- a/modules/cfxGroups.lua +++ b/modules/cfxGroups.lua @@ -146,8 +146,6 @@ end function cfxGroups.start() cfxGroups.fetchAllGroupsFromDCS() -- read all groups from mission. --- cfxGroups.showAllGroups() - trigger.action.outText("cfxGroups version " .. cfxGroups.version .. " started", 30) return true diff --git a/modules/cfxHeloTroops.lua b/modules/cfxHeloTroops.lua index b6ce258..a4525bd 100644 --- a/modules/cfxHeloTroops.lua +++ b/modules/cfxHeloTroops.lua @@ -1,5 +1,5 @@ cfxHeloTroops = {} -cfxHeloTroops.version = "2.1.0" +cfxHeloTroops.version = "2.2.0" cfxHeloTroops.verbose = false cfxHeloTroops.autoDrop = true cfxHeloTroops.autoPickup = false @@ -18,6 +18,10 @@ cfxHeloTroops.pickupRange = 100 -- meters -- -- 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 -- -- cfxHeloTroops -- a module to pick up and drop infantry. Can be used with any helo, @@ -40,10 +44,15 @@ cfxHeloTroops.requiredLibs = { cfxHeloTroops.unitConfigs = {} -- all configs are stored by unit's name cfxHeloTroops.myEvents = {3, 4, 5} -- 3- takeoff, 4 - land, 5 - crash -cfxHeloTroops.legalTroops = {"Soldier AK", "Infantry AK", "Infantry AK ver2", "Infantry AK ver3", "Infantry AK Ins", "Soldier M249", "Soldier M4 GRG", "Soldier M4", "Soldier RPG", "Paratrooper AKS-74", "Paratrooper RPG-16", "Stinger comm dsr", "Stinger comm", "Soldier stinger", "SA-18 Igla-S comm", "SA-18 Igla-S manpad", "Igla manpad INS", "SA-18 Igla comm", "SA-18 Igla manpad",} + +-- legalTroops now optional, else check against dcsCommon.typeIsInfantry +--cfxHeloTroops.legalTroops = {"Soldier AK", "Infantry AK", "Infantry AK ver2", "Infantry AK ver3", "Infantry AK Ins", "Soldier M249", "Soldier M4 GRG", "Soldier M4", "Soldier RPG", "Paratrooper AKS-74", "Paratrooper RPG-16", "Stinger comm dsr", "Stinger comm", "Soldier stinger", "SA-18 Igla-S comm", "SA-18 Igla-S manpad", "Igla manpad INS", "SA-18 Igla comm", "SA-18 Igla manpad",} cfxHeloTroops.troopWeight = 100 -- kg average weight per trooper +-- persistence support +cfxHeloTroops.deployedTroops = {} + function cfxHeloTroops.resetConfig(conf) conf.autoDrop = cfxHeloTroops.autoDrop --if true, will drop troops on-board upon touchdown conf.autoPickup = cfxHeloTroops.autoPickup -- if true will load nearest troops upon touchdown @@ -113,7 +122,7 @@ function cfxHeloTroops.preProcessor(event) -- make sure it has an initiator if not event.initiator then return false end -- no initiator local theUnit = event.initiator - if not cfxPlayer.isPlayerUnit(theUnit) then return false end -- not a player unit + if not dcsCommon.isPlayerUnit(theUnit) then return false end -- not a player unit local cat = theUnit:getCategory() if cat ~= Group.Category.HELICOPTER then return false end @@ -414,9 +423,16 @@ function cfxHeloTroops.addGroundMenu(conf) local typeArray = dcsCommon.splitString(theTypes, ',') typeArray = dcsCommon.trimArray(typeArray) local allLegal = true + -- check agianst default (dcsCommon) or own definition (if exists) for idy, aType in pairs(typeArray) do - if not dcsCommon.arrayContainsString(cfxHeloTroops.legalTroops, aType) then - allLegal = false + if cfxHeloTroops.legalTroops then + if not dcsCommon.arrayContainsString(cfxHeloTroops.legalTroops, aType) then + allLegal = false + end + else + if not dcsCommon.typeIsInfantry(aType) then + allLegal = false + end end end if allLegal then @@ -503,9 +519,16 @@ function cfxHeloTroops.filterTroopsByType(unitsToLoad) local pass = true for iT, sT in pairs(aT) do -- check if this is a valid type - if not dcsCommon.arrayContainsString(cfxHeloTroops.legalTroops, sT) then - pass = false - break + if cfxHeloTroops.legalTroops then + if not dcsCommon.arrayContainsString(cfxHeloTroops.legalTroops, sT) then + pass = false + break + end + else + if not dcsCommon.typeIsInfantry(sT) then + pass = false + break + end end end if pass then @@ -604,13 +627,21 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf) local chopperZone = cfxZones.createSimpleZone("choppa", p, 12) -- 12 m ratius around choppa --local theCoalition = theUnit:getCountry() -- make it choppers country local theCoalition = theUnit:getGroup():getCoalition() -- make it choppers COALITION - local theGroup = cfxZones.createGroundUnitsInZoneForCoalition ( + local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( theCoalition, conf.troopsOnBoard.name, -- dcsCommon.uuid("Assault"), -- maybe use config name as loaded from the group chopperZone, unitTypes, conf.dropFormation, 90) + -- persistence management + local troopData = {} + troopData.groupData = theData + troopData.orders = orders -- always set + troopData.side = theCoalition + troopData.range = range + cfxHeloTroops.deployedTroops[theData.name] = troopData + local troop = cfxGroundTroops.createGroundTroops(theGroup, range, orders) -- use default range and orders -- instead of scheduling tasking in one second, we add to -- ground troops pool, and the troop pool manager will assign some enemies @@ -663,8 +694,13 @@ function cfxHeloTroops.doLoadGroup(args) -- TODO: ensure compatibility with CSAR module group:destroy() + -- now immediately run a GC so this group is removed + -- from any save data + cfxHeloTroops.GC() + -- say so trigger.action.outTextForGroup(conf.id, "Team '".. conf.troopsOnBoard.name .."' aboard, ready to go!", 30) + -- reset menu cfxHeloTroops.removeComms(conf.unit) cfxHeloTroops.setCommsMenu(conf.unit) @@ -748,6 +784,32 @@ function cfxHeloTroops.playerChangeEvent(evType, description, player, data) end +-- +-- Regular GC and housekeeping +-- +function cfxHeloTroops.GC() + -- GC run. remove all my dead remembered troops + local filteredAttackers = {} + local before = #cfxHeloTroops.deployedTroops + for gName, gData in pairs (cfxHeloTroops.deployedTroops) do + -- all we need to do is get the group of that name + -- and if it still returns units we are fine + local gameGroup = Group.getByName(gName) + if gameGroup and gameGroup:isExist() and gameGroup:getSize() > 0 then + filteredAttackers[gName] = gData + end + end + cfxHeloTroops.deployedTroops = filteredAttackers + + if cfxHeloTroops.verbose then + trigger.action.outText("helo troops GC ran: before <" .. before .. ">, after <" .. #cfxHeloTroops.deployedTroops .. ">", 30) + end +end + +function cfxHeloTroops.houseKeeping() + timer.scheduleFunction(cfxHeloTroops.houseKeeping, {}, timer.getTime() + 5 * 60) -- every 5 minutes + cfxHeloTroops.GC() +end -- -- read config zone @@ -757,7 +819,7 @@ function cfxHeloTroops.readConfigZone() local theZone = cfxZones.getZoneByName("heloTroopsConfig") if not theZone then trigger.action.outText("+++heloT: no config zone!", 30) - return + theZone = cfxZones.createSimpleZone("heloTroopsConfig") end cfxHeloTroops.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) @@ -781,6 +843,60 @@ function cfxHeloTroops.readConfigZone() end +-- +-- Load / Save data +-- +function cfxHeloTroops.saveData() + local theData = {} + local allTroopData = {} + -- run a GC pre-emptively + cfxHeloTroops.GC() + -- now simply iterate and save all deployed troops + for gName, gData in pairs(cfxHeloTroops.deployedTroops) do + local sData = dcsCommon.clone(gData) + dcsCommon.synchGroupData(sData.groupData) + allTroopData[gName] = sData + end + theData.troops = allTroopData + return theData +end + +function cfxHeloTroops.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("cfxHeloTroops") + if not theData then + if cfxHeloTroops.verbose then + trigger.action.outText("+++heloT: no save date received, skipping.", 30) + end + return + end + + -- simply spawn all troops that we have carried around and + -- were still alive when we saved. Troops that were picked + -- up by helos never made it to the save file + local allTroopData = theData.troops + for gName, gdTroop in pairs (allTroopData) do + local gData = gdTroop.groupData + local orders = gdTroop.orders + local side = gdTroop.side + local range = gdTroop.range + local cty = gData.cty + local cat = gData.cat + + -- now spawn, but first + -- add to my own deployed queue so we can save later + local gdClone = dcsCommon.clone(gdTroop) + cfxHeloTroops.deployedTroops[gName] = gdClone + local theGroup = coalition.addGroup(cty, cat, gData) + -- post-proccing for cfxGroundTroops + + -- add to groundTroops + local newTroops = cfxGroundTroops.createGroundTroops(theGroup, range, orders) + cfxGroundTroops.addGroundTroopsToPool(newTroops) + end +end + + -- -- Start -- @@ -791,7 +907,11 @@ function cfxHeloTroops.start() return false end - -- read config zone + -- read config zone + cfxHeloTroops.readConfigZone() + + -- start housekeeping + cfxHeloTroops.houseKeeping() -- install callbacks for helo-relevant events dcsCommon.addEventHandler(cfxHeloTroops.somethingHappened, cfxHeloTroops.preProcessor, cfxHeloTroops.postProcessor) @@ -807,6 +927,18 @@ function cfxHeloTroops.start() cfxPlayer.addMonitor(cfxHeloTroops.playerChangeEvent) trigger.action.outText("cf/x Helo Troops v" .. cfxHeloTroops.version .. " started", 30) + + -- now load all save data and populate map with troops that + -- we deployed last save. + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = cfxHeloTroops.saveData + persistence.registerModule("cfxHeloTroops", callbacks) + -- now load my data + cfxHeloTroops.loadData() + end + return true end diff --git a/modules/cfxMX.lua b/modules/cfxMX.lua index 93e8759..9daeb2f 100644 --- a/modules/cfxMX.lua +++ b/modules/cfxMX.lua @@ -1,5 +1,5 @@ cfxMX = {} -cfxMX.version = "1.2.1" +cfxMX.version = "1.2.2" cfxMX.verbose = false --[[-- Mission data decoder. Access to ME-built mission structures @@ -18,7 +18,9 @@ cfxMX.verbose = false - added references for allFixed, allHelo, allGround, allSea, allStatic 1.2.1 - added countryByName - added linkByName - + 1.2.2 - fixed ctry bug in countryByName + - playerGroupByName + - playerUnitByName --]]-- cfxMX.groupNamesByID = {} cfxMX.groupIDbyName = {} @@ -29,7 +31,10 @@ cfxMX.allFixedByName = {} cfxMX.allHeloByName = {} cfxMX.allGroundByName = {} cfxMX.allSeaByName = {} -cfxMX.allStaticByName ={} +cfxMX.allStaticByName = {} + +cfxMX.playerGroupByName = {} -- returns data only if a player is in group +cfxMX.playerUnitByName = {} -- returns data only if this is a player unit function cfxMX.getGroupFromDCSbyName(aName, fetchOriginal) if not fetchOriginal then fetchOriginal = false end @@ -145,14 +150,11 @@ function cfxMX.getStaticFromDCSbyName(aName, fetchOriginal) theStatic.groupId = group_data.groupId -- copy linked unit data theStatic.linkUnit = linkUnit - end return theStatic, category, countryID, groupName - end -- if name match end -- for all units end -- has groups - end -- is a static end --if has category data end --if plane, helo etc... category @@ -207,7 +209,8 @@ function cfxMX.createCrossReferences() cfxMX.groupNamesByID[aID] = aName cfxMX.groupIDbyName[aName] = aID cfxMX.groupDataByName[aName] = group_data - cfxMX.countryByName[aName] = cntry_id + cfxMX.countryByName[aName] = countryID -- !!! was cntry_id + -- now make the type-specific xrefs if obj_type_name == "helicopter" then cfxMX.allHeloByName[aName] = group_data @@ -223,7 +226,18 @@ function cfxMX.createCrossReferences() -- should be impossible, but still trigger.action.outText("+++MX: <" .. obj_type_name .. "> unknown type for <" .. aName .. ">", 30) end - end + -- now iterate all units in this group + -- for player into + for unit_num, unit_data in pairs(group_data.units) do + if unit_data.skill then + if unit_data.skill == "Client" or unit_data.skill == "Player" then + -- player unit + cfxMX.playerUnitByName[unit_data.name] = unit_data + cfxMX.playerGroupByName[aName] = group_data -- inefficient, but works + end -- if unit skill client + end -- if has skill + end -- for all units + end -- for all groups end --if has category data end --if plane, helo etc... category end --for all objects in country diff --git a/modules/cfxOwnedZones.lua b/modules/cfxOwnedZones.lua index 1d21922..2cf280d 100644 --- a/modules/cfxOwnedZones.lua +++ b/modules/cfxOwnedZones.lua @@ -1,5 +1,5 @@ cfxOwnedZones = {} -cfxOwnedZones.version = "1.2.0" +cfxOwnedZones.version = "1.2.1" cfxOwnedZones.verbose = false cfxOwnedZones.announcer = true cfxOwnedZones.name = "cfxOwnedZones" @@ -45,6 +45,7 @@ cfxOwnedZones.name = "cfxOwnedZones" 1.2.0 - support for persistence - conq+1 --> conquered! - no cfxGroundTroop bug (no delay) +1.2.1 - fix in load to correctly re-establish all attackers for subsequent save --]]-- cfxOwnedZones.requiredLibs = { @@ -797,6 +798,7 @@ end function cfxOwnedZones.GC() -- GC run. remove all my dead remembered troops + local before = #cfxOwnedZones.spawnedAttackers local filteredAttackers = {} for gName, gData in pairs (cfxOwnedZones.spawnedAttackers) do -- all we need to do is get the group of that name @@ -807,6 +809,9 @@ function cfxOwnedZones.GC() end end cfxOwnedZones.spawnedAttackers = filteredAttackers + if cfxOwnedZones.verbose then + trigger.action.outText("owned zones GC ran: before <" .. before .. ">, after <" .. #cfxOwnedZones.spawnedAttackers .. ">", 30) + end end function cfxOwnedZones.update() @@ -967,7 +972,7 @@ function cfxOwnedZones.loadData() local cty = gData.cty local cat = gData.cat -- add to my own attacker queue so we can save later - local dClone = dcsCommon.clone(gData) + local dClone = dcsCommon.clone(gdTroop) cfxOwnedZones.spawnedAttackers[gName] = dClone local theGroup = coalition.addGroup(cty, cat, gData) if cfxGroundTroops then diff --git a/modules/cfxPlayerScore.lua b/modules/cfxPlayerScore.lua index f282120..393cd5b 100644 --- a/modules/cfxPlayerScore.lua +++ b/modules/cfxPlayerScore.lua @@ -1,5 +1,5 @@ cfxPlayerScore = {} -cfxPlayerScore.version = "1.3.1" +cfxPlayerScore.version = "1.4.0" cfxPlayerScore.badSound = "Death BRASS.wav" cfxPlayerScore.scoreSound = "Quest Snare 3.wav" cfxPlayerScore.announcer = true @@ -23,12 +23,15 @@ cfxPlayerScore.announcer = true number that is given under OBJECT ID when using assign as... 1.3.1 - isStaticObject() to better detect buildings after Match 22 patch + 1.3.2 - corrected ground default score + - removed dependency to cfxPlayer + 1.4.0 - persistence support + - better unit-->static switch support for generic type kill --]]-- cfxPlayerScore.requiredLibs = { "dcsCommon", -- this is doing score keeping - "cfxPlayer", -- player events, comms "cfxZones", -- zones for config } cfxPlayerScore.playerScore = {} -- init to empty @@ -46,14 +49,15 @@ cfxPlayerScore.typeScore = {} -- cfxPlayerScore.aircraft = 50 cfxPlayerScore.helo = 40 -cfxPlayer.ground = 10 +cfxPlayerScore.ground = 10 cfxPlayerScore.ship = 80 cfxPlayerScore.train = 5 + function cfxPlayerScore.cat2BaseScore(inCat) if inCat == 0 then return cfxPlayerScore.aircraft end -- airplane if inCat == 1 then return cfxPlayerScore.helo end -- helo - if inCat == 2 then return cfxPlayer.ground end -- ground + 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 @@ -69,7 +73,10 @@ function cfxPlayerScore.object2score(inVictim) -- does not have group if type(inName) == "number" then inName = tostring(inName) end - + + -- now, since 2.7x DCS turns units into static objects for + -- cooking off, so first thing we need to do is do a name check + local objectScore = cfxPlayerScore.typeScore[inName] if not objectScore then -- try the type desc @@ -80,6 +87,20 @@ function cfxPlayerScore.object2score(inVictim) -- does not have group 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 + if attributes then + if attributes["Vehicles"] or attributes["Ground vehicles"] or attributes["Ground Units"] then return cfxPlayerScore.ground end + if attributes["Helicopters"] then return cfxPlayerScore.helo end + if attributes["Planes"] then return cfxPlayerScore.aircraft end + if attributes["Ships"] then return cfxPlayerScore.ship end + -- trains can't be detected + end + if not objectScore then return 0 end return objectScore end @@ -224,7 +245,7 @@ function cfxPlayerScore.preProcessor(theEvent) return false end - local wasPlayer = cfxPlayer.isPlayerUnit(killer) + local wasPlayer = dcsCommon.isPlayerUnit(killer) return wasPlayer end return false @@ -245,7 +266,7 @@ function cfxPlayerScore.killDetected(theEvent) -- we are only getting called when and if -- a kill occured and killer was a player -- and target exists - +-- trigger.action.outText("KILL EVENT", 30) local killer = theEvent.initiator local killerName = killer:getPlayerName() if not killerName then killerName = "" end @@ -255,13 +276,14 @@ function cfxPlayerScore.killDetected(theEvent) local victim = theEvent.target -- was it a player kill? - local pk = cfxPlayer.isPlayerUnit(victim) + local pk = dcsCommon.isPlayerUnit(victim) -- was it a scenery object? local wasBuilding = dcsCommon.isSceneryObject(victim) if wasBuilding then -- these objects have no coalition; we simply award the score if -- it exists in look-up table. + --trigger.action.outText("KILL SCENERY", 30) local staticScore = cfxPlayerScore.object2score(victim) if staticScore > 0 then trigger.action.outSoundForCoalition(killSide, cfxPlayerScore.scoreSound) @@ -282,10 +304,11 @@ 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 -- name as entered in TOP LINE local staticScore = cfxPlayerScore.object2score(victim) +-- trigger.action.outText("KILL STATIC with score " .. staticScore, 30) if staticScore > 0 then -- this was a named static, return the score - unless our own if fraternicide then @@ -295,6 +318,7 @@ function cfxPlayerScore.killDetected(theEvent) trigger.action.outSoundForCoalition(killSide, cfxPlayerScore.scoreSound) end staticScore = scoreMod * staticScore + cfxPlayerScore.logKillForPlayer(killerName, victim) cfxPlayerScore.awardScoreTo(killSide, staticScore, killerName) else -- no score, no mentions @@ -368,7 +392,7 @@ function cfxPlayerScore.readConfigZone(theZone) -- default scores cfxPlayerScore.aircraft = cfxZones.getNumberFromZoneProperty(theZone, "aircraft", 50) cfxPlayerScore.helo = cfxZones.getNumberFromZoneProperty(theZone, "helo", 40) - cfxPlayer.ground = cfxZones.getNumberFromZoneProperty(theZone, "ground", 10) + cfxPlayerScore.ground = cfxZones.getNumberFromZoneProperty(theZone, "ground", 10) cfxPlayerScore.ship = cfxZones.getNumberFromZoneProperty(theZone, "ship", 80) cfxPlayerScore.train = cfxZones.getNumberFromZoneProperty(theZone, "train", 5) @@ -382,6 +406,36 @@ function cfxPlayerScore.readConfigZone(theZone) end end +-- +-- load / save +-- +function cfxPlayerScore.saveData() + local theData = {} + local theScore = dcsCommon.clone(cfxPlayerScore.playerScore) + theData.theScore = theScore + return theData +end + +function cfxPlayerScore.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("cfxPlayerScore") + if not theData then + if cfxPlayerScore.verbose then + trigger.action.outText("+++playerscore: no save date received, skipping.", 30) + end + return + end + + local theScore = theData.theScore + cfxPlayerScore.playerScore = theScore + +end + + +-- +-- start +-- + function cfxPlayerScore.start() if not dcsCommon.libCheck("cfx Player Score", cfxPlayerScore.requiredLibs) @@ -413,6 +467,18 @@ function cfxPlayerScore.start() dcsCommon.addEventHandler(cfxPlayerScore.killDetected, cfxPlayerScore.preProcessor, cfxPlayerScore.postProcessor) + + -- now load all save data and populate map with troops that + -- we deployed last save. + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = cfxPlayerScore.saveData + persistence.registerModule("cfxPlayerScore", callbacks) + -- now load my data + cfxPlayerScore.loadData() + end + trigger.action.outText("cfxPlayerScore v" .. cfxPlayerScore.version .. " started", 30) return true end diff --git a/modules/cfxSSBClient.lua b/modules/cfxSSBClient.lua index e7f92b6..cbd84e0 100644 --- a/modules/cfxSSBClient.lua +++ b/modules/cfxSSBClient.lua @@ -1,5 +1,5 @@ cfxSSBClient = {} -cfxSSBClient.version = "2.0.3" +cfxSSBClient.version = "2.1.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 @@ -40,6 +40,9 @@ Version History - added verbosity 2.0.3 - getPlayerName nil-trap on cloned player planes guard in onEvent + 2.1.0 - slotState + - persistence + WHAT IT IS SSB Client is a small script that forms the client-side counterpart to @@ -88,6 +91,7 @@ cfxSSBClient.playerGroups = {} cfxSSBClient.closedAirfields = {} -- list that closes airfields for any aircrafts cfxSSBClient.playerPlanes = {} -- names of units that a player is flying 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 function cfxSSBClient.closeAirfieldNamed(name) @@ -191,6 +195,7 @@ function cfxSSBClient.setSlotAccessForGroup(theGroup) end -- set the ssb flag for this group so the server can see it trigger.action.setUserFlag(theName, blockState) + cfxSSBClient.slotState[theName] = blockState if cfxSSBClient.verbose then trigger.action.outText("+++SSB: group ".. theName .. ": " .. comment, 30) end @@ -312,7 +317,7 @@ function cfxSSBClient:onEvent(event) -- block this slot. trigger.action.setUserFlag(gName, cfxSSBClient.disabledFlagValue) - + cfxSSBClient.slotState[gName] = cfxSSBClient.disabledFlagValue -- remember this plane to not re-enable if -- airfield changes hands later cfxSSBClient.crashedGroups[gName] = thePilot -- set to crash pilot @@ -425,6 +430,48 @@ function cfxSSBClient.readConfigZone() cfxSSBClient.disabledFlagValue = cfxZones.getNumberFromZoneProperty(theZone, "disabledFlagValue", cfxSSBClient.enabledFlagValue + 100) end +-- +-- load / save +-- +function cfxSSBClient.saveData() + local theData = {} + local states = dcsCommon.clone(cfxSSBClient.slotState) + local crashed = dcsCommon.clone(cfxSSBClient.crashedGroups) + theData.states = states + theData.crashed = crashed + return theData +end + +function cfxSSBClient.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("cfxSSBClient") + if not theData then + if cfxSSBClient.verbose then + trigger.action.outText("+++cfxSSB: no save date received, skipping.", 30) + end + return + end + + cfxSSBClient.slotState = theData.states + if not cfxSSBClient.slotState then + trigger.action.outText("SSBClient: nil slot state on load", 30) + cfxSSBClient.slotState = {} + end + for slot, state in pairs (cfxSSBClient.slotState) do + trigger.action.setUserFlag(slot, state) + if state > 0 and cfxSSBClient.verbose then + trigger.action.outText("SSB: blocked <" .. slot .. "> on load", 30) + end + end + + cfxSSBClient.crashedGroups = theData.crashed + if not cfxSSBClient.crashedGroups then + cfxSSBClient.crashedGroups = {} + trigger.action.outText("SSBClient: nil crashers on load", 30) + end + +end + -- -- start -- @@ -455,6 +502,16 @@ function cfxSSBClient.start() -- now turn on ssb trigger.action.setUserFlag("SSB",100) + -- persistence: load states + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = cfxSSBClient.saveData + persistence.registerModule("cfxSSBClient", callbacks) + -- now load my data + cfxSSBClient.loadData() + end + -- say hi! trigger.action.outText("cfxSSBClient v".. cfxSSBClient.version .. " running, SBB enabled", 30) diff --git a/modules/cfxSpawnZones.lua b/modules/cfxSpawnZones.lua index 255d2d3..499e806 100644 --- a/modules/cfxSpawnZones.lua +++ b/modules/cfxSpawnZones.lua @@ -1,15 +1,21 @@ cfxSpawnZones = {} -cfxSpawnZones.version = "1.6.0" +cfxSpawnZones.version = "1.7.0" cfxSpawnZones.requiredLibs = { "dcsCommon", -- common is of course needed for everything -- pretty stupid to check for this since we -- need common to invoke the check, but anyway "cfxZones", -- Zones, of course "cfxCommander", -- to make troops do stuff - "cfxGroundTroops", -- generic data module for weight + "cfxGroundTroops", -- for ordering then around } cfxSpawnZones.ups = 1 cfxSpawnZones.verbose = false + +-- persistence: all groups we ever spawned. +-- is regularly GC'd + +cfxSpawnZones.spawnedGroups = {} + -- -- Zones that conform with this requirements spawn toops automatically -- *** DOES NOT EXTEND ZONES *** LINKED OWNER via masterOwner *** @@ -54,6 +60,7 @@ cfxSpawnZones.verbose = false -- 1.5.2 - activate?, pause? flag -- 1.5.3 - spawn?, spawnUnits? flags -- 1.6.0 - trackwith interface for group tracker +-- 1.7.0 - persistence support -- -- new version requires cfxGroundTroops, where they are -- @@ -203,12 +210,14 @@ function cfxSpawnZones.createSpawner(inZone) theSpawner.formation = "circle_out" theSpawner.formation = cfxZones.getStringFromZoneProperty(inZone, "formation", "circle_out") theSpawner.paused = cfxZones.getBoolFromZoneProperty(inZone, "paused", false) - theSpawner.orders = cfxZones.getStringFromZoneProperty(inZone, "orders", "guard") + theSpawner.orders = cfxZones.getStringFromZoneProperty(inZone, "orders", "guard"):lower() --theSpawner.orders = cfxZones.getZoneProperty(inZone, "orders") -- used to assign special orders, default is 'guard', use "laze" to make them laze targets. can be 'wait-' which may auto-convert to 'guard' after pick-up by helo, to be handled outside. -- use "train" to tell them to HOLD WEAPONS, don't move and don't participate in loop, so we have in effect target dummies -- can also use order 'dummy' or 'dummies' to switch to train if theSpawner.orders:lower() == "dummy" or theSpawner.orders:lower() == "dummies" then theSpawner.orders = "train" end + if theSpawner.orders:lower() == "training" then theSpawner.orders = "train" end + theSpawner.range = cfxZones.getNumberFromZoneProperty(inZone, "range", 300) -- if we have a range, for example enemy detection for Lasing or engage range theSpawner.maxSpawns = cfxZones.getNumberFromZoneProperty(inZone, "maxSpawns", -1) -- if there is a limit on how many troops can spawn. -1 = endless spawns @@ -238,6 +247,7 @@ end function cfxSpawnZones.getSpawnerForZoneNamed(aName) local aZone = cfxZones.getZoneByName(aName) + if not aZone then return nil end return cfxSpawnZones.getSpawnerForZone(aZone) end @@ -331,7 +341,7 @@ function cfxSpawnZones.spawnWithSpawner(aSpawner) local theCoalition = coalition.getCountryCoalition(theCountry) -- trigger.action.outText("+++ spawn: coal <" .. theCoalition .. "> from country <" .. theCountry .. ">", 30) - local theGroup = cfxZones.createGroundUnitsInZoneForCoalition ( + local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( theCoalition, aSpawner.baseName .. "-" .. aSpawner.count, -- must be unique aSpawner.zone, @@ -340,15 +350,26 @@ function cfxSpawnZones.spawnWithSpawner(aSpawner) aSpawner.heading) aSpawner.theSpawn = theGroup aSpawner.count = aSpawner.count + 1 - -- we may also want to add this to auto ground troops pool - -- we not only want to, we absolutely need to in order - -- to make this work. + + -- isnert into collector for persistence + local troopData = {} + troopData.groupData = theData + troopData.orders = aSpawner.orders -- always set + troopData.side = theCoalition + troopData.target = aSpawner.target -- can be nil! + troopData.tracker = theZone.trackWith -- taken from ZONE!!, can be nil + troopData.range = aSpawner.range + cfxSpawnZones.spawnedGroups[theData.name] = troopData if aSpawner.orders and ( aSpawner.orders:lower() == "training" or aSpawner.orders:lower() == "train" ) then -- make them ROE "HOLD" + -- remember to do this in persistence as well! + -- they aren't fed to cfxGroundTroops. + -- we should update groundTroops to simply + -- drop those with 'train' or 'training' cfxCommander.scheduleOptionForGroup( theGroup, AI.Option.Ground.id.ROE, @@ -434,6 +455,23 @@ end -- -- U P D A T E -- +function cfxSpawnZones.GC() + -- GC run. remove all my dead remembered troops + local filteredAttackers = {} + local before = #cfxSpawnZones.spawnedGroups + for gName, gData in pairs (cfxSpawnZones.spawnedGroups) do + -- all we need to do is get the group of that name + -- and if it still returns units we are fine + local gameGroup = Group.getByName(gName) + if gameGroup and gameGroup:isExist() and gameGroup:getSize() > 0 then + filteredAttackers[gName] = gData + end + end + cfxSpawnZones.spawnedGroups = filteredAttackers + if cfxSpawnZones.verbose then + trigger.action.outText("spawn zones GC ran: before <" .. before .. ">, after <" .. #cfxSpawnZones.spawnedGroups .. ">", 30) + end +end function cfxSpawnZones.update() cfxSpawnZones.updateSchedule = timer.scheduleFunction(cfxSpawnZones.update, {}, timer.getTime() + 1/cfxSpawnZones.ups) @@ -445,9 +483,9 @@ function cfxSpawnZones.update() local group = spawner.theSpawn if group:isExist() then -- see how many members of this group are still alive - local liveUnits = dcsCommon.getLiveGroupUnits(group) + local liveUnits = group:getSize() --dcsCommon.getLiveGroupUnits(group) -- spawn is still alive, will not spawn - if #liveUnits > 1 then + if liveUnits > 1 then -- we may want to check if this member is still inside -- of spawn location. currently we don't do that needsSpawn = false @@ -526,6 +564,158 @@ function cfxSpawnZones.update() end end +function cfxSpawnZones.houseKeeping() + timer.scheduleFunction(cfxSpawnZones.houseKeeping, {}, timer.getTime() + 5 * 60) -- every 5 minutes + cfxSpawnZones.GC() +end + +-- +-- LOAD/SAVE +-- +function cfxSpawnZones.saveData() + local theData = {} + local allSpawnerData = {} + -- now iterate all spawners and collect their data + for theZone, theSpawner in pairs(cfxSpawnZones.allSpawners) do + local zName = theZone.name + local spawnData = {} + if theSpawner.spawn and theSpawner.spawn:isExist() then + spawnData.spawn = theSpawner.spawn:getName() + end + spawnData.count = theSpawner.count + spawnData.paused = theSpawner.paused + spawnData.cdStarted = theSpawner.cdStarted + spawnData.cdTimer = theSpawner.cdTimer - timer.getTime() -- what remains of the cooldown time + + allSpawnerData[zName] = spawnData + end + + -- run a GC + cfxSpawnZones.GC() + -- now collect all living groups + -- no longer required to check if group is lively + local allLivingTroopData = {} + for gName, gData in pairs(cfxSpawnZones.spawnedGroups) do + local sData = dcsCommon.clone(gData) + dcsCommon.synchGroupData(sData.groupData) + allLivingTroopData[gName] = sData + end + + theData.spawnerData = allSpawnerData + theData.troopData = allLivingTroopData + return theData +end + +function cfxSpawnZones.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("cfxSpawnZones") + if not theData then + if cfxSpawnZones.verbose then + trigger.action.outText("+++spwn: no save date received, skipping.", 30) + end + return + end + + -- we begin by re-spawning all spawned groups so that the + -- spwners can then later link to them + local allTroopData = theData.troopData + for gName, gdTroop in pairs (allTroopData) do + local gData = gdTroop.groupData + local orders = gdTroop.orders + local target = gdTroop.target + local tracker = gdTroop.tracker + local side = gdTroop.side + local range = gdTroop.range + local cty = gData.cty + local cat = gData.cat + + -- now spawn, but first + -- add to my own attacker queue so we can save later + local gdClone = dcsCommon.clone(gdTroop) + cfxSpawnZones.spawnedGroups[gName] = gdClone + local theGroup = coalition.addGroup(cty, cat, gData) + -- post-proccing for 'train' orders + if orders and (orders == "train" ) then + -- make them ROE "HOLD" + cfxCommander.scheduleOptionForGroup( + theGroup, + AI.Option.Ground.id.ROE, + AI.Option.Ground.val.ROE.WEAPON_HOLD, + 1.0) + else + -- add to groundTroops + local newTroops = cfxGroundTroops.createGroundTroops(theGroup, range, orders) + cfxGroundTroops.addGroundTroopsToPool(newTroops) + -- engage a target zone + if target then + local destZone = cfxZones.getZoneByName(target) + if destZone then + newTroops.destination = destZone + cfxGroundTroops.makeTroopsEngageZone(newTroops) + end + end + end + + -- post-proccing for trackwith [may not be needed when we] + -- have persistence in the tracking module. that module + -- simply schedules re-connecting after one second + end + + -- now set up all spawners with save data + local allSpawnerData = theData.spawnerData + for zName, sData in pairs (allSpawnerData) do + local theZone = cfxZones.getZoneByName(zName) + if theZone then + local theSpawner = cfxSpawnZones.getSpawnerForZone(theZone) + if theSpawner then + theSpawner.inited = true -- inited by persistence + theSpawner.count = sData.count + theSpawner.paused = sData.paused + theSpawner.cdStarted = sData.cdStarted + if theSpawner.cdStarted then + theSpawner.cdTimer = timer.getTime() + sData.cdTimer + else + theSpawner.cdTimer = -1 + end + if sData.spawn then + local theGroup = Group.getByName(sData.spawn) + if theGroup then + theSpawner.spawn = theGroup + else + trigger.action.outText("+++spwn (persistence): can't re-connect spawner <" .. zName .. "> with group <" .. sData.spawn .. ">, skipping", 30) + end + end + else + trigger.action.outText("+++spwn (persistence): can't find spawner for zone <" .. zName .. ">, skipping", 30) + end + else + trigger.action.outText("+++spwn (persistence): can't find zone <" .. zName .. "> for spawner, skipping", 30) + end + end + +end + +-- +-- START +-- +function cfxSpawnZones.initialSpawnCheck(aSpawner) + if not aSpawner.paused + and cfxSpawnZones.verifySpawnOwnership(aSpawner) + and aSpawner.maxSpawns ~= 0 + and not aSpawner.inited + then + cfxSpawnZones.spawnWithSpawner(aSpawner) + -- update spawn count and make sure we haven't spawned the one and only + if aSpawner.maxSpawns > 0 then + aSpawner.maxSpawns = aSpawner.maxSpawns - 1 + end + if aSpawner.maxSpawns == 0 then + aSpawner.paused = true + trigger.action.outText("+++ maxspawn -- turning off zone " .. aSpawner.zone.name, 30) + end + end +end + function cfxSpawnZones.start() if not dcsCommon.libCheck("cfx Spawn Zones", cfxSpawnZones.requiredLibs) then @@ -540,22 +730,29 @@ function cfxSpawnZones.start() for k, aZone in pairs(attrZones) do local aSpawner = cfxSpawnZones.createSpawner(aZone) cfxSpawnZones.addSpawner(aSpawner) - if not aSpawner.paused and cfxSpawnZones.verifySpawnOwnership(aSpawner) and aSpawner.maxSpawns ~= 0 then - cfxSpawnZones.spawnWithSpawner(aSpawner) - -- update spawn count and make sure we haven't spawned the one and only - if aSpawner.maxSpawns > 0 then - aSpawner.maxSpawns = aSpawner.maxSpawns - 1 - end - if aSpawner.maxSpawns == 0 then - aSpawner.paused = true - trigger.action.outText("+++ maxspawn -- turning off zone " .. aSpawner.zone.name, 30) - end - end + end + + -- we now do persistence + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = cfxSpawnZones.saveData + persistence.registerModule("cfxSpawnZones", callbacks) + -- now load my data + cfxSpawnZones.loadData() + end + + -- we now spawn if not taken care of by load / save + for theZone, aSpawner in pairs(cfxSpawnZones.allSpawners) do + cfxSpawnZones.initialSpawnCheck(aSpawner) end -- and start the regular update calls cfxSpawnZones.update() + -- start housekeeping + cfxSpawnZones.houseKeeping() + trigger.action.outText("cfx Spawn Zones v" .. cfxSpawnZones.version .. " started.", 30) return true end diff --git a/modules/changer.lua b/modules/changer.lua index 8bc3b45..262b924 100644 --- a/modules/changer.lua +++ b/modules/changer.lua @@ -1,5 +1,5 @@ changer = {} -changer.version = "1.0.2" +changer.version = "1.0.3" changer.verbose = false changer.ups = 1 changer.requiredLibs = { @@ -12,6 +12,7 @@ changer.changers = {} 1.0.0 - Initial version 1.0.1 - Better guards in config to avoid Zone getter warning 1.0.2 - on/off: verbosity + 1.0.3 - NOT on/off Transmogrify an incoming signal to an output signal - not @@ -111,6 +112,12 @@ function changer.createChangerWithZone(theZone) if cfxZones.hasProperty(theZone, "changeOn/Off?") then theZone.changerOnOff = cfxZones.getStringFromZoneProperty(theZone, "changeOn/Off?", "*", 1) end + if cfxZones.hasProperty(theZone, "NOT On/Off?") then + theZone.changerOnOffINV = cfxZones.getStringFromZoneProperty(theZone, "NOT On/Off?", "*", 1) + end + if cfxZones.hasProperty(theZone, "NOT changeOn/Off?") then + theZone.changerOnOffINV = cfxZones.getStringFromZoneProperty(theZone, "NOT changeOn/Off?", "*", 1) + end end -- @@ -207,6 +214,10 @@ function changer.update() end -- do processing if not paused + if aZone.changerOnOff and aZone.changerOnOffINV then + trigger.action.outText("+++chgr: WARNING - zone <" .. aZone.name .. "> has conflicting change On/off inputs, disregating inverted input (NOT changeOn/off?)", 30) + end + if not aZone.changerPaused then if aZone.changerOnOff then if cfxZones.getFlagValue(aZone.changerOnOff, aZone) > 0 then @@ -216,7 +227,15 @@ function changer.update() trigger.action.outText("+++chgr: " .. aZone.name .. " gate closed.", 30) end end - else + elseif aZone.changerOnOffINV then + if cfxZones.getFlagValue(aZone.changerOnOffINV, aZone) == 0 then + changer.process(aZone) + else + if changer.verbose or aZone.verbose then + trigger.action.outText("+++chgr: " .. aZone.name .. " gate closed.", 30) + end + end + else changer.process(aZone) end end diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index 780d146..9ba1af8 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -1,5 +1,5 @@ cloneZones = {} -cloneZones.version = "1.4.9" +cloneZones.version = "1.5.0" cloneZones.verbose = false cloneZones.requiredLibs = { "dcsCommon", -- always @@ -17,6 +17,9 @@ cloneZones.callbacks = {} cloneZones.unitXlate = {} cloneZones.groupXlate = {} -- used to translate original groupID to cloned. only holds last spawned group id cloneZones.uniqueCounter = 9200000 -- we start group numbering here + +cloneZones.allClones = {} -- all clones spawned, regularly GC'd +cloneZones.allCObjects = {} -- all clones objects --[[-- Clones Groups from ME mission data Copyright (c) 2022 by Christian Franz and cf/x AG @@ -53,6 +56,7 @@ cloneZones.uniqueCounter = 9200000 -- we start group numbering here 1.4.8 - added 'wipe?' synonym 1.4.9 - onRoad option - rndHeading option + 1.5.0 - persistence --]]-- @@ -289,7 +293,7 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" theZone.onRoad = cfxZones.getBoolFromZoneProperty(theZone, "onRoad", false) if theZone.rndLoc and theZone.verbose then - trigger.action.outText("+++ rndloc on for " .. theZone.name, 30) + --trigger.action.outText("+++ rndloc on for " .. theZone.name, 30) end -- we end with clear plate @@ -767,6 +771,12 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) -- now spawn all raw data for idx, rawData in pairs (dataToSpawn) do -- now spawn and save to clones + -- first norm and clone data for later save + rawData.cty = rawData.CZctry + rawData.cat = rawData.CZtheCat + local theData = dcsCommon.clone(rawData) + cloneZones.allClones[rawData.name] = theData + local theGroup = coalition.addGroup(rawData.CZctry, rawData.CZtheCat, rawData) table.insert(spawnedGroups, theGroup) @@ -888,20 +898,25 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) end local isCargo = rawData.canCargo + rawData.cty = ctry + -- save for persistence + local theData = dcsCommon.clone(rawData) + cfxZones.allCObjects[rawData.name] = theData + local theStatic = coalition.addStaticObject(ctry, rawData) local newStaticID = tonumber(theStatic:getID()) table.insert(spawnedStatics, theStatic) -- we don't mix groups with units, so no lookup tables for -- statics if newStaticID == rawData.CZTargetID then --- trigger.action.outText("Static ID OK: " .. newStaticID .. " for " .. rawData.name, 30) + else trigger.action.outText("Static ID mismatch: " .. newStaticID .. " vs (target) " .. rawData.CZTargetID .. " for " .. rawData.name, 30) end cloneZones.unitXlate[origID] = newStaticID -- same as units cloneZones.invokeCallbacks(theZone, "did spawn static", theStatic) - --]]-- + if cloneZones.verbose or spawnZone.verbose then trigger.action.outText("Static spawn: spawned " .. aStaticName, 30) end @@ -1058,7 +1073,7 @@ function cloneZones.update() if aZone.deSpawnFlag then local currTriggerVal = cfxZones.getFlagValue(aZone.deSpawnFlag, aZone) -- trigger.misc.getUserFlag(aZone.deSpawnFlag) if currTriggerVal ~= aZone.lastDeSpawnValue then - if cloneZones.verbose then + if cloneZones.verbose or aZone.verbose then trigger.action.outText("+++clnZ: DEspawn triggered for <" .. aZone.name .. ">", 30) end cloneZones.despawnAll(aZone) @@ -1103,15 +1118,249 @@ function cloneZones.onStart() --trigger.action.outText("+++clnZ: Enter atStart", 30) for idx, theZone in pairs(cloneZones.cloners) do if theZone.onStart then - if cloneZones.verbose then - trigger.action.outText("+++clnZ: atStart will spawn for <"..theZone.name .. ">", 30) + if theZone.isStarted then + if cloneZones.verbose or theZone.verbose then + trigger.action.outText("+++clnz: onStart pre-empted for <" .. theZone.name .. "> by persistence", 30) + end + else + if cloneZones.verbose or theZone.verbose then + trigger.action.outText("+++clnZ: onStart spawing for <"..theZone.name .. ">", 30) + end + cloneZones.spawnWithCloner(theZone) end - cloneZones.spawnWithCloner(theZone) - end end end +-- +-- Regular GC and housekeeping +-- +function cloneZones.GC() + -- GC run. remove all my dead remembered troops + local filteredAttackers = {} + for gName, gData in pairs (cloneZones.allClones) do + -- all we need to do is get the group of that name + -- and if it still returns units we are fine + local gameGroup = Group.getByName(gName) + if gameGroup and gameGroup:isExist() and gameGroup:getSize() > 0 then + -- we now filter for categories. we currently only let + -- ground units pass + -- better make this configurabele by option later + if gData.cat == 0 and false then -- block aircraft + elseif gData.cat == 1 and false then -- block helos + elseif gData.cat == 2 and false then -- block ground + elseif gData.cat == 3 and false then -- block ship + elseif gData.cat == 4 and false then -- block trains + else + -- not filtered, persist + filteredAttackers[gName] = gData + end + end + end + cloneZones.allClones = filteredAttackers + + filteredAttackers = {} + for gName, gData in pairs (cloneZones.allCObjects) do + -- all we need to do is get the group of that name + -- and if it still returns units we are fine + local theObject = StaticObject.getByName(gName) + if theObject and theObject:isExist() then + filteredAttackers[gName] = gData + if theObject:getLife() < 1 then + gData.dead = true + end + end + end + cloneZones.allCObjects = filteredAttackers +end + +function cloneZones.houseKeeping() + timer.scheduleFunction(cloneZones.houseKeeping, {}, timer.getTime() + 5 * 60) -- every 5 minutes + cloneZones.GC() +end + + +-- +-- LOAD / SAVE +-- +function cloneZones.synchGroupMXData(theData) + -- we iterate the group's units one by one and update them + local newUnits = {} + local allUnits = theData.units + for idx, unitData in pairs(allUnits) do + local uName = unitData.name + local gUnit = Unit.getByName(uName) + if gUnit and gUnit:isExist() then + unitData.heading = dcsCommon.getUnitHeading(gUnit) + pos = gUnit:getPoint() + unitData.x = pos.x + unitData.y = pos.z -- (!!) + -- add aircraft handling here (alt, speed etc) + -- perhaps even curtail route + table.insert(newUnits, unitData) + end + end + theData.units = newUnits +end + +function cloneZones.synchMXObjData(theData) + local oName = theData.name + local theObject = StaticObject.getByName(oName) + theData.heading = dcsCommon.getUnitHeading(theObject) + pos = theObject:getPoint() + theData.x = pos.x + theData.y = pos.z -- (!!) + theData.isDead = theObject:getLife() < 1 + theData.dead = theData.isDead +end + +function cloneZones.saveData() + local theData = {} + local allCloneData = {} + local allSOData = {} + -- run a GC pre-emptively + cloneZones.GC() + + -- now simply iterate and save all deployed clones + for gName, gData in pairs(cloneZones.allClones) do + local sData = dcsCommon.clone(gData) + cloneZones.synchGroupMXData(sData) + allCloneData[gName] = sData + end + + -- now simply iterate and save all deployed clones + for gName, gData in pairs(cloneZones.allCObjects) do + local sData = dcsCommon.clone(gData) + cloneZones.synchMXObjData(sData) + allSOData[gName] = sData + end + + -- now save all cloner stati + local cloners = {} + for idx, theCloner in pairs(cloneZones.cloners) do + local cData = {} + local cName = theCloner.name + -- mySpawns: all groups i'm curently observing for empty! + -- myStatics: dto for objects + local mySpawns = {} + for idx, aGroup in pairs(theCloner.mySpawns) do + if aGroup and aGroup:isExist() and aGroup:getSize() > 0 then + table.insert(mySpawns, aGroup:getName()) + end + end + cData.mySpawns = mySpawns + local myStatics = {} + for idx, aStatic in pairs(theCloner.myStatics) do + table.insert(myStatics, aStatic:getName()) + end + cData.myStatics = myStatics + cData.isStarted = theCloner.isStarted -- to prevent onStart + cloners[cName] = cData + end + + + -- save globals + theData.cuid = cloneZones.uniqueCounter -- replace whatever is larger + theData.uuid = dcsCommon.simpleUUID -- replace whatever is larger + + -- save to struct and pass back + theData.clones = allCloneData + theData.objects = allSOData + theData.cloneZones = cloners + return theData +end + +function cloneZones.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("cloneZones") + if not theData then + if cloneZones.verbose then + trigger.action.outText("+++clnZ: no save date received, skipping.", 30) + end + return + end + + -- spawn all units + local allClones = theData.clones + for gName, gData in pairs (allClones) do + local cty = gData.cty + local cat = gData.cat + + -- now spawn, but first + -- add to my own deployed queue so we can save later + local gdClone = dcsCommon.clone(gData) + cloneZones.allClones[gName] = gdClone + local theGroup = coalition.addGroup(cty, cat, gData) + end + + -- spawn all static objects + local allObjects = theData.objects + for oName, oData in pairs(allObjects) do + local newStatic = dcsCommon.clone(oData) + -- add link info if it exists + newStatic.linkUnit = cfxMX.linkByName[oName] + if newStatic.linkUnit and unitPersistence.verbose then + trigger.action.outText("+++unitPersistence: linked static <" .. oName .. "> to unit <" .. newStatic.linkUnit .. ">", 30) + end + local cty = staticData.cty +-- local cat = staticData.cat + -- spawn new one, replacing same.named old, dead if required + gStatic = coalition.addStaticObject(cty, newStatic) + + -- processing for cargoManager + if oData.canCargo then + if cfxCargoManager then + cfxCargoManager.addCargo(gStatic) + end + end + + -- add the original data block to be remembered + -- for next save + cloneZones.allCObjects[oName] = oData + end + + -- now update all spawners and reconnect them with their spawns + local allCloners = theData.cloneZones + for cName, cData in pairs(allCloners) do + local theCloner = cloneZones.getCloneZoneByName(cName) + if theCloner then + theCloner.isStarted = true -- ALWAYS TRUE WHEN WE COME HERE! cData.isStarted + local mySpawns = {} + for idx, aName in pairs(cData.mySpawns) do + local theGroup = Group.getByName(aName) + if theGroup then + table.insert(mySpawns, theGroup) + else + trigger.action.outText("+++clnZ - persistence: can't reconnect cloner <" .. cName .. "> with clone group <".. aName .. ">", 30) + end + end + theCloner.mySpawns = mySpawns + + local myStatics = {} + for idx, aName in pairs(cData.myStatics) do + local theStatic = StaticObject.getByName(aName) + if theStatic then + table.insert(myStatics, theStatic) + else + trigger.action.outText("+++clnZ - persistence: can't reconnect cloner <" .. cName .. "> with static <".. aName .. ">", 30) + end + end + theCloner.myStatics = myStatics + else + trigger.action.outText("+++clnZ - persistence: cannot synch cloner <" .. cName .. ">, does not exist", 30) + end + end + + -- finally, synch uid and uuid + if theData.cuid and theData.cuid > cloneZones.uniqueCounter then + cloneZones.uniqueCounter = theData.cuid + end + if theData.uuiD and theData.uuid > dcsCommon.simpleUUID then + dcsCommon.simpleUUID = theData.uuid + end + +end + -- -- START -- @@ -1155,16 +1404,28 @@ function cloneZones.start() cloneZones.addCloneZone(aZone) -- remember it so we can smoke it end - -- run through onStart, but leave at least a few - -- cycles to go through object removal so statics - -- can spawn on ground. onStart is being deprecated, the - -- raiseFlag module covers this since the first time - -- raiseFlag is run is t0 + 0.5s + -- update all cloners and spawned clones from file + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = cloneZones.saveData + persistence.registerModule("cloneZones", callbacks) + -- now load my data + cloneZones.loadData() + end + + -- schedule onStart, and leave at least a few + -- cycles to go through object removal + -- persistencey has loaded isStarted if a cloner was + -- already started timer.scheduleFunction(cloneZones.onStart, {}, timer.getTime() + 0.1) -- start update cloneZones.update() + -- start housekeeping + cloneZones.houseKeeping() + trigger.action.outText("cfx Clone Zones v" .. cloneZones.version .. " started.", 30) return true end diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index d7958f2..f15a249 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "2.7.0" +dcsCommon.version = "2.7.1" --[[-- VERSION HISTORY 2.2.6 - compassPositionOfARelativeToB - clockPositionOfARelativeToB @@ -89,6 +89,9 @@ dcsCommon.version = "2.7.0" - new getSceneryObjectInZoneByName() 2.7.0 - new synchGroupData() clone, topClone and copyArray now all nil-trap + 2.7.1 - new isPlayerUnit() -- moved from cfxPlayer + new getAllExistingPlayerUnitsRaw - from cfxPlayer + new typeIsInfantry() --]]-- @@ -103,7 +106,8 @@ dcsCommon.version = "2.7.0" -- globals dcsCommon.cbID = 0 -- callback id for simple callback scheduling dcsCommon.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P"} -- Ka-50 and Gazelle can't carry troops - + dcsCommon.coalitionSides = {0, 1, 2} + -- verify that a module is loaded. obviously not required -- for dcsCommon, but all higher-order modules function dcsCommon.libCheck(testingFor, requiredLibs) @@ -2248,6 +2252,28 @@ function dcsCommon.isTroopCarrier(theUnit) return false end +function dcsCommon.isPlayerUnit(theUnit) + -- new patch. simply check if getPlayerName returns something + if not theUnit then return false end + if not theUnit.getPlayerName then return false end -- map/static object + local pName = theUnit:getPlayerName() + if pName then return true end + return false +end + +function dcsCommon.getAllExistingPlayerUnitsRaw() + local apu = {} + for idx, theSide in pairs(dcsCommon.coalitionSides) do + local thePlayers = coalition.getPlayers(theSide) + for idy, theUnit in pairs (thePlayers) do + if theUnit and theUnit:isExist() then + table.insert(apu, theUnit) + end + end + end + return apu +end + function dcsCommon.getUnitAlt(theUnit) if not theUnit then return 0 end if not theUnit:isExist() then return 0 end @@ -2335,10 +2361,7 @@ function dcsCommon.getUnitHeadingDegrees(theUnit) return heading * 57.2958 -- 180 / math.pi end -function dcsCommon.unitIsInfantry(theUnit) - if not theUnit then return false end - if not theUnit:isExist() then return end - local theType = theUnit:getTypeName() +function dcsCommon.typeIsInfantry(theType) local isInfantry = dcsCommon.containsString(theType, "infantry", false) or dcsCommon.containsString(theType, "paratrooper", false) or @@ -2349,6 +2372,23 @@ function dcsCommon.unitIsInfantry(theUnit) return isInfantry end +function dcsCommon.unitIsInfantry(theUnit) + if not theUnit then return false end + if not theUnit:isExist() then return end + local theType = theUnit:getTypeName() +--[[-- + local isInfantry = + dcsCommon.containsString(theType, "infantry", false) or + dcsCommon.containsString(theType, "paratrooper", false) or + dcsCommon.containsString(theType, "stinger", false) or + dcsCommon.containsString(theType, "manpad", false) or + dcsCommon.containsString(theType, "soldier", false) or + dcsCommon.containsString(theType, "SA-18 Igla", false) + return isInfantry +--]]-- + return dcsCommon.typeIsInfantry(theType) +end + function dcsCommon.coalition2county(inCoalition) -- simply return UN troops for 0 neutral, -- joint red for 1 red diff --git a/modules/groupTrackers.lua b/modules/groupTrackers.lua index e082718..da7fe33 100644 --- a/modules/groupTrackers.lua +++ b/modules/groupTrackers.lua @@ -1,5 +1,5 @@ groupTracker = {} -groupTracker.version = "1.1.3" +groupTracker.version = "1.1.4" groupTracker.verbose = false groupTracker.ups = 1 groupTracker.requiredLibs = { @@ -20,6 +20,12 @@ groupTracker.trackers = {} 1.1.3 - spellings - addGroupToTrackerNamed bug removed accessing tracker - new removeGroupNamedFromTrackerNamed() + 1.1.4 - destroy? input + - allGone! output + - triggerMethod + - method + - isDead optimization + --]]-- function groupTracker.addTracker(theZone) @@ -87,7 +93,7 @@ function groupTracker.addGroupToTrackerNamed(theGroup, trackerName) return end - if not theGroup:isExist() then + if not Group.isExist(theGroup) then trigger.action.outText("+++gTrk: group does not exist when adding to tracker <" .. trackerName .. ">", 30) return end @@ -147,6 +153,17 @@ function groupTracker.createTrackerWithZone(theZone) theZone.trackedGroups = {} + theZone.trackerMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") + if cfxZones.hasProperty(theZone, "trackerMethod") then + theZone.trackerMethod = cfxZones.getStringFromZoneProperty(theZone, "trackerMethod", "inc") + end + + theZone.trackerTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") + + if cfxZones.hasProperty(theZone, "trackerTriggerMethod") then + theZone.trackerTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "trackerTriggerMethod", "change") + end + if cfxZones.hasProperty(theZone, "numGroups") then theZone.tNumGroups = cfxZones.getStringFromZoneProperty(theZone, "numGroups", "*") -- we may need to zero this flag @@ -181,6 +198,16 @@ function groupTracker.createTrackerWithZone(theZone) end end + if cfxZones.hasProperty(theZone, "destroy?") then + theZone.destroyFlag = cfxZones.getStringFromZoneProperty(theZone, "destroy?", "*") + theZone.lastDestroyValue = cfxZones.getFlagValue(theZone.destroyFlag, theZone) + end + + if cfxZones.hasProperty(theZone, "allGone!") then + theZone.allGoneFlag = cfxZones.getStringFromZoneProperty(theZone, "allGone!", "") -- note string on number default + end + theZone.lastGroupCount = 0 + if theZone.verbose or groupTracker.verbose then trigger.action.outText("gTrck: processed <" .. theZone.name .. ">", 30) end @@ -189,12 +216,23 @@ end -- -- update -- +function groupTracker.destroyAllInZone(theZone) + for idx, theGroup in pairs(theZone.trackedGroups) do + if Group.isExist(theGroup) then + theGroup:destroy() + end + end + -- we keep all groups in trackedGroups so we + -- generate a host of destroy events when we run through + -- checkGroups next +end + function groupTracker.checkGroups(theZone) local filteredGroups = {} for idx, theGroup in pairs(theZone.trackedGroups) do -- see if this group can be transferred local isDead = false - if theGroup:isExist() then +--[[ if theGroup.isExist and theGroup:isExist() then local allUnits = theGroup:getUnits() isDead = true for idy, aUnit in pairs(allUnits) do @@ -203,6 +241,8 @@ function groupTracker.checkGroups(theZone) break end end +--]]-- + if Group.isExist(theGroup) and theGroup:getSize() > 0 then else isDead = true -- no longer exists end @@ -238,7 +278,22 @@ function groupTracker.update() timer.scheduleFunction(groupTracker.update, {}, timer.getTime() + 1/groupTracker.ups) for idx, theZone in pairs(groupTracker.trackers) do + + if theZone.destroyFlag and cfxZones.testZoneFlag(theZone, theZone.destroyFlag, theZone.trackerTriggerMethod, "lastDestroyValue") then + groupTracker.destroyAllInZone(theZone) + if groupTracker.verbose or theZone.verbose then + trigger.action.outText("+++gTrk: destroying all groups tracked with <" .. theZone.name .. ">", 30) + end + end + groupTracker.checkGroups(theZone) + + -- see if we need to bang on empty! + local currCount = #theZone.trackedGroups + if theZone.allGoneFlag and currCount == 0 and currCount ~= theZone.lastGroupCount then + cfxZones.pollFlag(aZone.allGoneFlag, aZone.trackerMethod, aZone) + end + theZone.lastGroupCount = currCount end end diff --git a/modules/limitedAirframes.lua b/modules/limitedAirframes.lua index be88ac3..7797d77 100644 --- a/modules/limitedAirframes.lua +++ b/modules/limitedAirframes.lua @@ -1,5 +1,5 @@ limitedAirframes = {} -limitedAirframes.version = "1.4.0" +limitedAirframes.version = "1.5.0" limitedAirframes.verbose = false limitedAirframes.enabled = true -- can be turned off limitedAirframes.userCanToggle = true -- F10 menu? @@ -18,7 +18,7 @@ limitedAirframes.requiredLibs = { -- pretty stupid to check for this since we -- need common to invoke the check, but anyway "cfxZones", -- Zones, of course for safe landings - "cfxPlayer", + -- "cfxPlayer", no longer needed } --[[-- VERSION HISTORY @@ -48,8 +48,9 @@ limitedAirframes.requiredLibs = { - 1.4.0 - DML integration, verbosity, clean-up, QoL improvements redSafe, blueSafe with attribute, backward compatible currRed + - 1.4.1 - removed dependency to cfxPlayer + - 1.5.0 - persistence support - --]]-- -- limitedAirframes manages the number of available player airframes @@ -123,6 +124,7 @@ function limitedAirframes.readConfigZone() end -- remember me limitedAirframes.config = theZone + limitedAirframes.name = "limitedAirframes" -- so we can call cfxZones with ourself as param limitedAirframes.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) @@ -131,22 +133,22 @@ function limitedAirframes.readConfigZone() end -- ok, for each property, load it if it exists - if cfxZones.hasProperty(theZone, "enabled") then +-- if cfxZones.hasProperty(theZone, "enabled") then limitedAirframes.enabled = cfxZones.getBoolFromZoneProperty(theZone, "enabled", true) - end +-- end - if cfxZones.hasProperty(theZone, "userCanToggle") then +-- if cfxZones.hasProperty(theZone, "userCanToggle") then limitedAirframes.userCanToggle = cfxZones.getBoolFromZoneProperty(theZone, "userCanToggle", true) - end +-- end - if cfxZones.hasProperty(theZone, "maxRed") then +-- if cfxZones.hasProperty(theZone, "maxRed") then limitedAirframes.maxRed = cfxZones.getNumberFromZoneProperty(theZone, "maxRed", -1) - end +-- end - if cfxZones.hasProperty(theZone, "maxBlue") then +-- if cfxZones.hasProperty(theZone, "maxBlue") then limitedAirframes.maxBlue = cfxZones.getNumberFromZoneProperty(theZone, "maxBlue", -1) - end +-- end limitedAirframes.numRed = cfxZones.getStringFromZoneProperty(theZone, "#red", "*none") limitedAirframes.numBlue = cfxZones.getStringFromZoneProperty(theZone, "#blue", "*none") @@ -318,7 +320,7 @@ function limitedAirframes.preProcessor(event) end - if not cfxPlayer.isPlayerUnit(theUnit) then + if not dcsCommon.isPlayerUnit(theUnit) then -- not a player unit. Events 5 and 6 have been -- handled before, so we can safely ignore return false @@ -828,6 +830,37 @@ function limitedAirframes.pilotsRescued(theCoalition, success, numRescued, notes trigger.action.outSoundForCoalition(theCoalition, limitedAirframes.warningSound)--"Quest Snare 3.wav") end +-- +-- Load / Save +-- +function limitedAirframes.saveData() + local theData = {} + theData.currRed = limitedAirframes.currRed + theData.currBlue = limitedAirframes.currBlue + return theData +end + +function limitedAirframes.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("limitedAirframes") + if not theData then + if limitedAirframes.verbose then + trigger.action.outText("+++limA: no save date received, skipping.", 30) + end + return + end + + if theData.currRed then + limitedAirframes.currRed = theData.currRed + end + + if theData.currBlue then + limitedAirframes.currBlue = theData.currBlue + end + +end + + -- -- START -- @@ -885,7 +918,7 @@ function limitedAirframes.start() limitedAirframes.currBlue = limitedAirframes.maxBlue -- collect active player unit names - local allPlayerUnits = cfxPlayer.getAllExistingPlayerUnitsRaw() + local allPlayerUnits = dcsCommon.getAllExistingPlayerUnitsRaw() for i=1, #allPlayerUnits do local aUnit = allPlayerUnits[i] limitedAirframes.addPlayerUnit(aUnit) @@ -905,6 +938,16 @@ function limitedAirframes.start() else trigger.action.outText("+++limA: NO CSAR integration", 30) end + + -- persistence: load states + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = limitedAirframes.saveData + persistence.registerModule("limitedAirframes", callbacks) + -- now load my data + limitedAirframes.loadData() + end -- say hi trigger.action.outText("cf/x Limited Airframes v" .. limitedAirframes.version .. " started: R:".. limitedAirframes.maxRed .. "/B:" .. limitedAirframes.maxBlue, 30) diff --git a/modules/persistence.lua b/modules/persistence.lua index 8d487cf..8b55f41 100644 --- a/modules/persistence.lua +++ b/modules/persistence.lua @@ -1,5 +1,5 @@ persistence = {} -persistence.version = "1.0.1" +persistence.version = "1.0.2" persistence.ups = 1 -- once every 1 seconds persistence.verbose = false persistence.active = false @@ -21,6 +21,7 @@ persistence.requiredLibs = { - spelling - cfxZones interface - always output save location + 1.0.2 - QoL when verbosity is on PROVIDES LOAD/SAVE ABILITY TO MODULES @@ -304,9 +305,9 @@ function persistence.missionStartDataLoad() -- we are done for now. modules check in -- after persistence and load their own data -- when they detect that there is data to load - if persistence.verbose then - trigger.action.outText("+++persistence: basic import complete.", 30) - end + + trigger.action.outText("+++persistence: successfully read mission save data", 30) + end -- @@ -345,6 +346,10 @@ function persistence.saveMissionData() if persistence.verbose then trigger.action.outText("+++persistence: gathered data from <" .. moduleName .. ">", 30) end + else + if persistence.verbose then + trigger.action.outText("+++persistence: NO DATA gathered data from <" .. moduleName .. ">, module returned NIL", 30) + end end end diff --git a/modules/radioMenus.lua b/modules/radioMenus.lua index 6154484..9c3094e 100644 --- a/modules/radioMenus.lua +++ b/modules/radioMenus.lua @@ -88,44 +88,7 @@ function radioMenu.createRadioMenuWithZone(theZone) if theZone.menuVisible then radioMenu.installMenu(theZone) end - --[[-- - -- now do the two options - local menuA = cfxZones.getStringFromZoneProperty(theZone, "itemA", "") - if theZone.coalition == 0 then - theZone.menuA = missionCommands.addCommand(menuA, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "A"}) - else - theZone.menuA = missionCommands.addCommandForCoalition(theZone.coalition, menuA, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "A"}) - end - - if cfxZones.hasProperty(theZone, "itemB") then - local menuB = cfxZones.getStringFromZoneProperty(theZone, "itemB", "") - if theZone.coalition == 0 then - theZone.menuB = missionCommands.addCommand(menuB, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "B"}) - else - theZone.menuB = missionCommands.addCommandForCoalition(theZone.coalition, menuB, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "B"}) - end - end - if cfxZones.hasProperty(theZone, "itemC") then - local menuC = cfxZones.getStringFromZoneProperty(theZone, "itemC", "") - if theZone.coalition == 0 then - theZone.menuC = missionCommands.addCommand(menuC, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "C"}) - else - theZone.menuC = missionCommands.addCommandForCoalition(theZone.coalition, menuC, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "C"}) - end - end - - if cfxZones.hasProperty(theZone, "itemD") then - local menuD = cfxZones.getStringFromZoneProperty(theZone, "itemD", "") - if theZone.coalition == 0 then - theZone.menuD = missionCommands.addCommand(menuD, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "D"}) - else - theZone.menuD = missionCommands.addCommandForCoalition(theZone.coalition, menuD, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "D"}) - end - end - - --]]-- - -- get the triggers & methods here theZone.radioMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") if cfxZones.hasProperty(theZone, "radioMethod") then diff --git a/modules/sixthSense.lua b/modules/sixthSense.lua new file mode 100644 index 0000000..bf9e102 --- /dev/null +++ b/modules/sixthSense.lua @@ -0,0 +1,18 @@ +sixthSense = {} +sixthSense.version = "1.0.0" +-- sniff out dead events and log them +function sixthSense:onEvent(event) + if event.id == 8 then -- S_EVENT_DEAD + if event.initiator then + local theObject = event.initiator + trigger.action.outText("DEAD event: " .. theObject:getName(), 30) + + else + trigger.action.outText("DEAD event, no initiator", 30) + end + end +end + +-- add event handler +world.addEventHandler(sixthSense) +trigger.action.outText("sixthSense v" .. sixthSense.version .. " loaded.", 30) diff --git a/modules/theDebugger.lua b/modules/theDebugger.lua index 0948c8f..5bafaba 100644 --- a/modules/theDebugger.lua +++ b/modules/theDebugger.lua @@ -1289,4 +1289,5 @@ end -q d.e.f returns string "asdasda..." -q sada reuturs + - xref: which zones/attributes reference a flag, g.g. '-xref go' --]]-- diff --git a/modules/unitPersistence.lua b/modules/unitPersistence.lua index 56414f0..cbb3d2d 100644 --- a/modules/unitPersistence.lua +++ b/modules/unitPersistence.lua @@ -1,5 +1,5 @@ unitPersistence = {} -unitPersistence.version = '1.0.1' +unitPersistence.version = '1.1.1' unitPersistence.verbose = false unitPersistence.updateTime = 60 -- seconds. Once every minute check statics unitPersistence.requiredLibs = { @@ -15,6 +15,11 @@ unitPersistence.requiredLibs = { - handles linked static objects - does no longer mess with heliports - update statics once a minute, not second + 1.0.2 - fixes coalition bug for static objects + 1.1.0 - added air and sea units - for filtering destroyed units + 1.1.1 - fixed static link (again) + - fixed air spawn (fixed wing) + REQUIRES PERSISTENCE AND MX @@ -23,6 +28,10 @@ unitPersistence.requiredLibs = { --]]-- unitPersistence.groundTroops = {} -- local buffered copy that we -- maintain from save to save +unitPersistence.fixedWing = {} +unitPersistence.rotorWing = {} +unitPersistence.ships = {} + unitPersistence.statics = {} -- locally unpacked and buffered static objects -- @@ -49,18 +58,24 @@ function unitPersistence.saveData() local uName = theUnitData.name local gUnit = Unit.getByName(uName) if gUnit and gUnit:isExist() then - -- got a live one! - gotALiveOne = true - -- update x and y and heading - theUnitData.heading = dcsCommon.getUnitHeading(gUnit) - pos = gUnit:getPoint() - theUnitData.x = pos.x - theUnitData.y = pos.z -- (!!) - -- ground units do not use alt + if gUnit:isActive() then + theUnitData.isDead = gUnit:getLife() < 1 + if not theUnitData.isDead then + -- got a live one! + gotALiveOne = true + -- update x, y and heading + theUnitData.heading = dcsCommon.getUnitHeading(gUnit) + pos = gUnit:getPoint() + theUnitData.x = pos.x + theUnitData.y = pos.z -- (!!) + end + else + gotALiveOne = true -- not yet activated + end else theUnitData.isDead = true end -- is alive and exists? - end -- unit not dead + end -- unit maybe not dead end -- iterate units in group groupData.isDead = not gotALiveOne end -- if group is not dead @@ -69,6 +84,136 @@ function unitPersistence.saveData() end end + -- aircraft + for groupName, groupData in pairs(unitPersistence.fixedWing) do + -- we update this record live and save it to file + if not groupData.isDead then + local gotALiveOne = false + local allUnits = groupData.units + for idx, theUnitData in pairs(allUnits) do + if not theUnitData.isDead then + local uName = theUnitData.name + local gUnit = Unit.getByName(uName) + if gUnit and gUnit:isExist() then + -- update x and y and heading if active and alive + if gUnit:isActive() then -- only overwrite if active + theUnitData.isDead = gUnit:getLife() < 1 + if not theUnitData.isDead then + gotALiveOne = true -- this group is still alive + theUnitData.heading = dcsCommon.getUnitHeading(gUnit) + pos = gUnit:getPoint() + theUnitData.x = pos.x + theUnitData.y = pos.z -- (!!) + theUnitData.alt = pos.y + theUnitData.alt_type = "BARO" + theUnitData.speed = dcsCommon.vMag(gUnit:getVelocity()) + -- we now could get fancy and do some proccing of the + -- waypoints and make the one it's nearest to its + -- current waypoint, curtailing all others, but that + -- may easily mess with waypoint actions, so we don't + end + else + gotALiveOne = true -- has not yet been activated, live + end + else + theUnitData.isDead = true + -- trigger.action.outText("+++unitPersistence - unit <" .. uName .. "> of group <" .. groupName .. "> is dead or non-existant", 30) + end -- is alive and exists? + end -- unit maybe not dead + end -- iterate units in group + groupData.isDead = not gotALiveOne + end -- if group is not dead + if unitPersistence.verbose then + trigger.action.outText("unitPersistence: save - processed air group <" .. groupName .. ">.", 30) + end + end + + -- helos + for groupName, groupData in pairs(unitPersistence.rotorWing) do + -- we update this record live and save it to file + if not groupData.isDead then + local gotALiveOne = false + local allUnits = groupData.units + for idx, theUnitData in pairs(allUnits) do + if not theUnitData.isDead then + local uName = theUnitData.name + local gUnit = Unit.getByName(uName) + if gUnit and gUnit:isExist() then + -- update x and y and heading if active and alive + if gUnit:isActive() then -- only overwrite if active + theUnitData.isDead = gUnit:getLife() < 1 + if not theUnitData.isDead then + gotALiveOne = true -- this group is still alive + theUnitData.heading = dcsCommon.getUnitHeading(gUnit) + pos = gUnit:getPoint() + theUnitData.x = pos.x + theUnitData.y = pos.z -- (!!) + theUnitData.alt = pos.y + theUnitData.alt_type = "BARO" + theUnitData.speed = dcsCommon.vMag(gUnit:getVelocity()) + -- we now could get fancy and do some proccing of the + -- waypoints and make the one it's nearest to its + -- current waypoint, curtailing all others, but that + -- may easily mess with waypoint actions, so we don't + end + else + gotALiveOne = true -- has not yet been activated, live + end + else + theUnitData.isDead = true + trigger.action.outText("+++unitPersistence - unit <" .. uName .. "> of group <" .. groupName .. "> is dead or non-existant", 30) + end -- is alive and exists? + end -- unit maybe not dead + end -- iterate units in group + groupData.isDead = not gotALiveOne + end -- if group is not dead + if unitPersistence.verbose then + trigger.action.outText("unitPersistence: save - processed helo group <" .. groupName .. ">.", 30) + end + end + + -- ships + for groupName, groupData in pairs(unitPersistence.ships) do + -- we update this record live and save it to file + if not groupData.isDead then + local gotALiveOne = false + local allUnits = groupData.units + for idx, theUnitData in pairs(allUnits) do + if not theUnitData.isDead then + local uName = theUnitData.name + local gUnit = Unit.getByName(uName) + if gUnit and gUnit:isExist() then + -- update x and y and heading if active and alive + if gUnit:isActive() then -- only overwrite if active + theUnitData.isDead = gUnit:getLife() < 1 + if not theUnitData.isDead then + gotALiveOne = true -- this group is still alive + theUnitData.heading = dcsCommon.getUnitHeading(gUnit) + pos = gUnit:getPoint() + theUnitData.x = pos.x + theUnitData.y = pos.z -- (!!) + -- we only filter dead ships and don't mess with others + -- during load, so we are doing this solely for possible + -- later expansions + end + else + gotALiveOne = true -- has not yet been activated, live + end + else + theUnitData.isDead = true + if unitPersistence.verbose then + trigger.action.outText("+++unitPersistence - unit <" .. uName .. "> of group <" .. groupName .. "> is dead or non-existant", 30) + end + end -- is alive and exists? + end -- unit maybe not dead + end -- iterate units in group + groupData.isDead = not gotALiveOne + end -- if group is not dead + if unitPersistence.verbose then + trigger.action.outText("unitPersistence: save - processed ship group <" .. groupName .. ">.", 30) + end + end + -- process all static objects placed with ME for oName, oData in pairs(unitPersistence.statics) do if not oData.isDead or oData.lateActivation then @@ -96,6 +241,10 @@ function unitPersistence.saveData() theData.version = unitPersistence.version theData.ground = unitPersistence.groundTroops + theData.fixedWing = unitPersistence.fixedWing + theData.rotorWing = unitPersistence.rotorWing + theData.ships = unitPersistence.ships + theData.statics = unitPersistence.statics return theData end @@ -103,6 +252,13 @@ end -- -- Load Mission Data -- +function unitPersistence.delayedSpawn(args) + local cat = args.cat + local cty = args.cty + local newGroup = args.newGroup + local theGroup = coalition.addGroup(cty, cat, newGroup) +end + function unitPersistence.loadMission() local theData = persistence.getSavedDataForModule("unitPersistence") if not theData then @@ -137,7 +293,6 @@ function unitPersistence.loadMission() -- filter all dead groups if theUnitData.isDead then -- skip it - else -- add it to new group table.insert(newUnits, theUnitData) @@ -145,29 +300,129 @@ function unitPersistence.loadMission() end -- replace old unit setup with new newGroup.units = newUnits - local cty = groupData.cty local cat = groupData.cat - - -- destroy the old group - --theGroup:destroy() -- will be replaced - - -- spawn new one + + -- spawn new one, replaces old one theGroup = coalition.addGroup(cty, cat, newGroup) if not theGroup then - trigger.action.outText("+++ failed to add modified group <" .. groupName .. ">") - end - if unitPersistence.verbose then - -- trigger.action.outText("+++unitPersistence: updated group <" .. groupName .. "> of cat <" .. cat .. "> for cty <" .. cty .. ">", 30) + trigger.action.outText("+++ failed to add modified group <" .. groupName .. ">", 30) end end end + unitPersistence.groundTroops = theData.ground else if unitPersistence.verbose then trigger.action.outText("+++unitPersistence: no ground unit data.", 30) end end + if theData.fixedWing then + for groupName, groupData in pairs(theData.fixedWing) do + --trigger.action.outText("+++ start loading group <" .. groupName .. ">", 30) + local theGroup = Group.getByName(groupName) + if not theGroup then + mismatchWarning = true + elseif groupData.isDead then + theGroup:destroy() + elseif groupData.isPlayer then + -- skip it + else + local newGroup = dcsCommon.clone(groupData) + local newUnits = {} + for idx, theUnitData in pairs(groupData.units) do + -- filter all dead groups + if theUnitData.isDead then + -- skip it + else + -- add it to new group + table.insert(newUnits, theUnitData) + end + end + -- replace old unit setup with (delayed) new + newGroup.units = newUnits + local cty = groupData.cty + local cat = groupData.cat + + -- spawn new one, replaces old one + theGroup:destroy() + local args = {} + args.cty = cty + args.cat = cat + args.newGroup = newGroup + -- since DCS can't replace a group directly (none will appear), we introduce a brief interval for things to settle + timer.scheduleFunction(unitPersistence.delayedSpawn, args, timer.getTime()+0.5) + + end + end + unitPersistence.fixedWing = theData.fixedWing + else + if unitPersistence.verbose then + trigger.action.outText("+++unitPersistence: no aircraft (fixed wing) unit data.", 30) + end + end + + if theData.rotorWing then + for groupName, groupData in pairs(theData.rotorWing) do + local theGroup = Group.getByName(groupName) + if not theGroup then + mismatchWarning = true + elseif groupData.isDead then + theGroup:destroy() + elseif groupData.isPlayer then + -- skip it + else + local newGroup = dcsCommon.clone(groupData) + local newUnits = {} + for idx, theUnitData in pairs(groupData.units) do + -- filter all dead groups + if theUnitData.isDead then + -- skip it + else + -- add it to new group + table.insert(newUnits, theUnitData) + end + end + -- replace old unit setup with new + newGroup.units = newUnits + local cty = groupData.cty + local cat = groupData.cat + + -- spawn new one, replaces old one + theGroup = coalition.addGroup(cty, cat, newGroup) + if not theGroup then + trigger.action.outText("+++ failed to add modified group <" .. groupName .. ">", 30) + end + end + end + unitPersistence.rotorWing = theData.rotorWing + else + if unitPersistence.verbose then + trigger.action.outText("+++unitPersistence: no rotor wing unit data.", 30) + end + end + + if theData.ships then + for groupName, groupData in pairs(theData.ships) do + local theGroup = Group.getByName(groupName) + if not theGroup then + mismatchWarning = true + elseif groupData.isDead then + -- when entire group is destroyed, we will also + -- destroy group. Else all survive + -- we currently don't dick around with carrieres unless they are dead + theGroup:destroy() + else + -- do nothing + end + end + unitPersistence.ships = theData.ships + else + if unitPersistence.verbose then + trigger.action.outText("+++unitPersistence: no rotor wing unit data.", 30) + end + end + -- and now the same for static objects if theData.statics then for name, staticData in pairs(theData.statics) do @@ -177,8 +432,6 @@ function unitPersistence.loadMission() if unitPersistence.verbose then trigger.action.outText("+++unitPersistence: static <" .. name .. "> is late activate, no update", 30) end - --elseif not theStatic then - -- mismatchWarning = true elseif staticData.category == "Heliports" then -- FARPS are static objects that HATE to be -- messed with, so we don't @@ -188,7 +441,7 @@ function unitPersistence.loadMission() else local newStatic = dcsCommon.clone(staticData) -- add link info if it exists - newStatic.linkUnit = cfxMX.linkByName[name] + newStatic.linkUnit = cfxMX.linkByName[staticData.groupName] if newStatic.linkUnit and unitPersistence.verbose then trigger.action.outText("+++unitPersistence: linked static <" .. name .. "> to unit <" .. newStatic.linkUnit .. ">", 30) end @@ -197,15 +450,16 @@ function unitPersistence.loadMission() -- spawn new one, replacing same.named old, dead if required gStatic = coalition.addStaticObject(cty, newStatic) if not gStatic then - trigger.action.outText("+++ failed to add modified static <" .. name .. ">") + trigger.action.outText("+++ failed to add modified static <" .. name .. ">", 30) end if unitPersistence.verbose then local note = "" if newStatic.dead then note = " (dead)" end - -- trigger.action.outText("+++unitPersistence: updated static <" .. name .. "> for cty <" .. cty .. ">" .. note, 30) + trigger.action.outText("+++unitPersistence: updated static <" .. name .. "> for cty <" .. cty .. ">" .. note, 30) end end end + unitPersistence.statics = theData.statics end if mismatchWarning then @@ -268,6 +522,7 @@ function unitPersistence.start() -- create a local copy of the entire groundForces data that -- we maintain internally. It's fixed, and we work on our -- own copy for speed + unitPersistence.groundTroops = {} for gname, data in pairs(cfxMX.allGroundByName) do local gd = dcsCommon.clone(data) -- copy the record gd.isDead = false -- init new field to alive @@ -275,14 +530,71 @@ function unitPersistence.start() gd.cat = cfxMX.catText2ID("vehicle") local gGroup = Group.getByName(gname) if not gGroup then - trigger.action.outText("+++warning: group <" .. gname .. "> does not exist in-game!?", 30) + trigger.action.outText("+++warning: ground group <" .. gname .. "> does not exist in-game!?", 30) else - local firstUnit = gGroup:getUnit(1) - gd.cty = firstUnit:getCountry() + gd.cty = cfxMX.countryByName[gname] unitPersistence.groundTroops[gname] = gd end end + -- now add all aircraft + unitPersistence.fixedWing = {} + for gname, data in pairs(cfxMX.allFixedByName) do + local gd = dcsCommon.clone(data) -- copy the record + gd.isDead = false -- init new field to alive + gd.isPlayer = (cfxMX.playerGroupByName[gname] ~= nil) + -- coalition and country + gd.cat = cfxMX.catText2ID("plane") -- 0 + gd.cty = cfxMX.countryByName[gname] + local gGroup = Group.getByName(gname) + if gd.isPlayer then + -- skip + elseif not gGroup then + trigger.action.outText("+++warning: fixed-wing group <" .. gname .. "> does not exist in-game!?", 30) + else + unitPersistence.fixedWing[gname] = gd + end + end + + -- and helicopters + unitPersistence.rotorWing = {} + for gname, data in pairs(cfxMX.allHeloByName) do + local gd = dcsCommon.clone(data) -- copy the record + gd.isDead = false -- init new field to alive + gd.isPlayer = (cfxMX.playerGroupByName[gname] ~= nil) + -- coalition and country + gd.cat = cfxMX.catText2ID("helicopter") -- 1 + gd.cty = cfxMX.countryByName[gname] + local gGroup = Group.getByName(gname) + if gd.isPlayer then + -- skip + elseif not gGroup then + trigger.action.outText("+++warning: helo group <" .. gname .. "> does not exist in-game!?", 30) + else + unitPersistence.rotorWing[gname] = gd + end + end + + -- finally ships + -- we only do ships to remove them when they are dead because + -- messing with ships can give problems: aircraft carriers. + unitPersistence.ships = {} + for gname, data in pairs(cfxMX.allSeaByName) do + local gd = dcsCommon.clone(data) -- copy the record + gd.isDead = false -- init new field to alive + -- coalition and country + gd.cat = cfxMX.catText2ID("ship") -- 3 + gd.cty = cfxMX.countryByName[gname] + local gGroup = Group.getByName(gname) + if gd.isPlayer then + -- skip + elseif not gGroup then + trigger.action.outText("+++warning: ship group <" .. gname .. "> does not exist in-game!?", 30) + else + unitPersistence.ships[gname] = gd + end + end + -- make local copies of all static MX objects -- that we also maintain internally, and convert them to game -- spawnable objects @@ -294,19 +606,18 @@ function unitPersistence.start() local theStatic = dcsCommon.clone(staticData) theStatic.isDead = false theStatic.groupId = mxData.groupId + theStatic.groupName = name -- save top-level name theStatic.cat = cfxMX.catText2ID("static") theStatic.cty = cfxMX.countryByName[name] + --trigger.action.outText("Processed MX static group <" .. name .. ">, object <" .. name .. "> with cty <" .. theStatic.cty .. ">",30) local gameOb = StaticObject.getByName(theStatic.name) if not gameOb then if unitPersistence.verbose then trigger.action.outText("+++unitPersistence: static object <" .. theStatic.name .. "> has late activation", 30) end theStatic.lateActivation = true - else - --theStatic.cty = gameOb:getCountry() - --unitPersistence.statics[theStatic.name] = theStatic end - unitPersistence.statics[theStatic.name] = theStatic + unitPersistence.statics[theStatic.name] = theStatic -- HERE WE CHANGE FROM GROUP NAME TO STATIC NAME!!! end end @@ -328,6 +639,11 @@ if not unitPersistence.start() then unitPersistence = nil end --[[-- - ToDo: linked statics and linked units on restore + - waypoint analysis for aircraft so + - whan they have take off as Inital WP and they are moving + then we change the first WP to 'turning point' + - waypoint analysis to match waypoint to position. very difficult if waypoints describe a circle. + - group analysis for carriers to be able to process groups that do + not contain carriers --]]--