diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 1710560..bb439de 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 ecb650b..0c21c42 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/cfxReconMode.lua b/modules/cfxReconMode.lua index 82913e2..7782b6d 100644 --- a/modules/cfxReconMode.lua +++ b/modules/cfxReconMode.lua @@ -1,5 +1,5 @@ cfxReconMode = {} -cfxReconMode.version = "2.2.0" +cfxReconMode.version = "2.2.1" cfxReconMode.verbose = false -- set to true for debug info cfxReconMode.reconSound = "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" -- to be played when somethiong discovered @@ -89,6 +89,7 @@ VERSION HISTORY 2.2.0 - new marksLocked config attribute, defaults to false - new marksFadeAfter config attribute to control mark time - dmlZones OOP upgrade + 2.2.1 - fixed "cfxReconSMode" typo cfxReconMode is a script that allows units to perform reconnaissance @@ -657,7 +658,7 @@ function cfxReconMode.doDeActivate() end end -function cfxReconSMode.updateQueues() +function cfxReconMode.updateQueues() -- schedule next call timer.scheduleFunction(cfxReconMode.updateQueues, {}, timer.getTime() + 1/cfxReconMode.ups) diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index 6dde883..d25ad99 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "4.0.0" +cfxZones.version = "4.0.2" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -150,6 +150,8 @@ cfxZones.version = "4.0.0" - immediate method switched to preceeding '#', to resolve conflict witzh negative numbers, backwards compatibility with old (dysfunctional) method - 4.0.1 - dmlZone:getName() +- 4.0.2 - removed verbosity from declutterZone (both versions) + --]]-- -- @@ -963,17 +965,17 @@ end function cfxZones.declutterZone(theZone) if not theZone then return end local theVol = cfxZones.getZoneVolume(theZone) - if theZone.verbose then - dcsCommon.dumpVar2Str("vol", theVol) - end +-- if theZone.verbose then +-- dcsCommon.dumpVar2Str("vol", theVol) +-- end world.removeJunk(theVol) end function dmlZone:declutterZone() local theVol = cfxZones.getZoneVolume(self) - if self.verbose then - dcsCommon.dumpVar2Str("vol", theVol) - end +-- if self.verbose then +-- dcsCommon.dumpVar2Str("vol", theVol) +-- end world.removeJunk(theVol) end diff --git a/modules/duel.lua b/modules/duel.lua index 1048bb5..5f43d83 100644 --- a/modules/duel.lua +++ b/modules/duel.lua @@ -1,14 +1,16 @@ duel = {} -duel.version = "1.0.0" +duel.version = "1.0.2" duel.verbose = false duel.requiredLibs = { "dcsCommon", "cfxZones", "cfxMX", -} +} --[[-- Version History 1.0.0 - Initial Version + 1.0.1 - verbosity bug with SSB removed + 1.0.2 - units are reserved for player when they disappear --]]-- @@ -16,13 +18,16 @@ duel.requiredLibs = { ATTENTION! - REQUIRES that SSB is running on the host - REQUIRTES that SSB is confgured that '0' (zero) means slot is enabled (this is SSB default) + - REQUIRES MULTIPLAYER (kind of obvious...) - This script must run at MISSION START and will enable SSB --]]-- duel.duelZones = {} -duel.activeDuelists = {} -duel.allDuelists = {} +duel.activePlayers = {} -- by player name +--duel.activeUnits = {} -- as above, by unit name +--duel.missingPlayers = {} +duel.allDuelists = {} -- all potential dualists as collected from zones -- -- reading attributes -- @@ -45,7 +50,7 @@ function duel.createDuelZone(theZone) duelist.groupName = groupData.name duelist.coa = cfxMX.groupCoalitionByName[duelist.groupName] if duel.verbose then - trigger.action.outText("Detected player unit <" .. duelist.name .. ">, type <" .. duelist.type .. "> of group <" .. duelist.groupName .. "> of coa <" .. duelist.coa .. "> in zone <" .. theZone.name .. "> as duelist", 30) +-- trigger.action.outText("Detected player unit <" .. duelist.name .. ">, type <" .. duelist.type .. "> of group <" .. duelist.groupName .. "> of coa <" .. duelist.coa .. "> in zone <" .. theZone.name .. "> as duelist", 30) end duelist.active = false @@ -63,6 +68,20 @@ function duel.createDuelZone(theZone) end theZone.state = "waiting" -- FSM, init to waiting state + theZone.duelTriggerMethod = theZone:getStringFromZoneProperty("duelTriggerMethod", "change") + if theZone:hasProperty("on?") then + theZone.duelOnFlag = theZone:getStringFromZoneProperty("on?", "*none") + theZone.lastDuelOn = theZone:getFlagValue(theZone.duelOnFlag) + end + if theZone:hasProperty("off?") then + theZone.duelOffFlag = theZone:getStringFromZoneProperty("off?", "*none") + theZone.lastDuelOff = theZone:getFlagValue(theZone.duelOffFlag) + end + theZone.onStart = theZone:getBoolFromZoneProperty("onStart", true) + theZone.active = true + if not theZone.onStart then + theZone.active = false + end end @@ -77,8 +96,8 @@ function duel.closeSlotsForZoneAndCoaExceptGroupNamed(theZone, coa, groupName) if (theDuelist.coa == coa) and (dgName ~= groupName) then if duel.verbose then trigger.action.outText("+++duel: closing SSB slot for group <" .. dgName .. ">, coa <" .. theDuelist.coa .. ">", 30) - trigger.action.setUserFlag(dgName,100) -- anything but 0 means closed end + trigger.action.setUserFlag(dgName,100) -- anything but 0 means closed end end end @@ -90,8 +109,8 @@ function duel.openSlotsForZoneAndCoa(theZone, coa) if (theDuelist.coa == coa) then if duel.verbose then trigger.action.outText("+++duel: opening SSB slot for group <" .. theDuelist.groupName .. ">, coa <" .. theDuelist.coa .. ">", 30) - trigger.action.setUserFlag(theDuelist.groupName, 0) -- 0 means OPEN end + trigger.action.setUserFlag(theDuelist.groupName, 0) -- 0 means OPEN end end end @@ -112,8 +131,15 @@ function duel.checkReopenSlotsForZoneAndCoa(theZone, coa) end if allUnengaged then + if duel.verbose then + trigger.action.outText("+++duel: will open all slots for <" .. theZone:getName() .. ">, coa <" .. coa .. ">", 30) + end duel.openSlotsForZoneAndCoa(theZone, coa) theZone.state = "waiting" + else + if duel.verbose then + trigger.action.outText("+++duel: unable to reopenslots for <" .. theZone:getName() .. ">, coa <" .. coa .. ">, still engaged", 30) + end end end @@ -132,8 +158,45 @@ function duel.duelistEnteredArena(theUnit, theDuelist) trigger.action.outText("Player <" .. player .. "> entered arena <" .. theZone:getName() .. "> in unit <" .. unitName .. "> of group <" .. groupName .. "> type <" .. theDuelist.type .. ">, belongs to coalition <" .. coa .. ">", 30) end - -- close all slots for this zone and coalition - duel.closeSlotsForZoneAndCoaExceptGroupNamed(theZone, coa, groupName) + -- remember this player should they go missing + local playerData = {} + playerData.playerName = player + playerData.unitName = unitName + playerData.lastSeen = timer.getTime() + playerData.theZone = theZone + playerData.coa = coa + + -- see if we are updating an existing player. + -- this will require a cleanup of the last time they + -- were here + if duel.activePlayers[player] then + -- we need to update slots and flags if player has chosen a + -- different unit + local lastData = duel.activePlayers[player] + if lastData.unitName ~= unitName then + if duel.verbose then + trigger.action.outText("Duel: player changed slots. Cleaning up", 30) + end + duel.checkReopenSlotsForZoneAndCoa(lastData.theZone, lastData.coa) + else + if duel.verbose then + trigger.action.outText("Duel: player re-slotted, no update required", 30) + end + end + end + duel.activePlayers[player] = playerData + + -- close all slots for this zone and coalition if it is active + if theZone.active then + if theZone.verbose or duel.verbose then + trigger.action.outText("+++duel: zone <" .. theZone:getName() .. ">, closing coa <" .. coa .. "> slots except for player's <" .. player .. "> group <" .. groupName .. ">", 30) + end + duel.closeSlotsForZoneAndCoaExceptGroupNamed(theZone, coa, groupName) + else + if theZone.verbose or duel.verbose then + trigger.action.outText("+++duel: zone <" .. theZone:getName() .. "> currently not active, not closing slots", 30) + end + end end @@ -155,6 +218,12 @@ function duel:onEvent(event) -- unit that entered is player controlled, and duelist duel.duelistEnteredArena(theUnit, duel.allDuelists[unitName]) end + + if event.id == 21 then + if duel.verbose then + trigger.action.outText("DUEL: player left unit <" .. theUnit:getName() .. ">", 30) + end + end end -- @@ -166,6 +235,7 @@ function duel.update() timer.scheduleFunction(duel.update, {}, timer.getTime() + 1/duel.ups) -- find units that have disappeared, and react accordingly + --[[-- for unitName, theDuelist in pairs (duel.allDuelists) do local theZone = theDuelist.zone if theDuelist.active then @@ -185,10 +255,54 @@ function duel.update() end end end + --]]-- + + -- now check the active players and their units + local now = timer.getTime() + local filtered = {} + for playerName, playerData in pairs(duel.activePlayers) do + local unitName = playerData.unitName + local theUnit = Unit.getByName(unitName) + if theUnit and Unit.isExist(theUnit) then + -- all is well, nothing to do except update time stamp + playerData.lastSeen = now + filtered[playerName] = playerData + else + -- unit has disappeared. let's see how long + local delta = math.floor(now - playerData.lastSeen) + if duel.verbose then + trigger.action.outText("player <" .. playerName .. ">'s unit is gone for <" .. delta .. "> seconds now.", 30) + end + -- if gone long enough, open all slots and delete player entry + if delta < (duel.keepSlot + 1) then + filtered[playerName] = playerData -- remember me + else + if duel.verbose then + trigger.action.outText("Time's up, all slots reopen now, player lost tabs on <" .. unitName .. ">", 30) + end + -- update duelist data (if required) + + -- open all slots in that zone for player's coa + duel.checkReopenSlotsForZoneAndCoa(playerData.theZone, playerData.coa) + + -- not remembered + end + end + end + duel.activePlayers = filtered -- now handle FSM for each zone separately for zoneName, theZone in pairs(duel.duelZones) do - + -- first, check if they have been turned on or off + if theZone:testZoneFlag(theZone.duelOnFlag, theZone.duelTriggerMethod, "lastDuelOn") then + theZone.active = true + end + + if theZone:testZoneFlag(theZone.duelOffFlag, theZone.duelTriggerMethod, "lastDuelOff") then + theZone.active = false + duel.openSlotsForZoneAndCoa(theZone, 1) + duel.openSlotsForZoneAndCoa(theZone, 2) + end end end @@ -205,6 +319,8 @@ function duel.readConfigZone() duel.verbose = theZone.verbose duel.ups = theZone:getNumberFromZoneProperty("ups", 1) + duel.keepSlot = theZone:getNumberFromZoneProperty("keepSlot", 30) -- grace period (in seconds) after unit vanishes in which they can re-slot via Briefing screen + duel.inside = theZone:getBoolFromZoneProperty("inside", true) duel.gracePeriod = theZone:getNumberFromZoneProperty("gracePeriod", 30) duel.keepScore = theZone:getBoolFromZoneProperty("score", true) diff --git a/modules/noGap.lua b/modules/noGap.lua new file mode 100644 index 0000000..43fb8ea --- /dev/null +++ b/modules/noGap.lua @@ -0,0 +1,406 @@ +noGap = {} +noGap.version = "1.0.0" + +noGap.verbose = false +noGap.ignoreMe = "-ng" -- ignore altogether +noGap.spIgnore = "-sp" -- only single-player ignored +noGap.isMP = false +noGap.enabled = true +noGap.timeOut = 0 -- in seconds, after that static restores, set to 0 to disable + +noGap.requiredLibs = { + "dcsCommon", + "cfxZones", + "cfxMX", +} +--[[-- + Written and (c) 2023 by Christian Franz + + Based on stopGap. Unlike stopGap, noGap + works on unit-level (stop-Gap works on group level) + Advantage: multiple-ship player groups look better, less code + Disadvantage: incompatibe with SSB/slotBlock + + What it does: + Replace all player units with static aircraft until the first time + that a player slots into that plane. Static is then replaced with live player unit. + + DOES NOT SUPPORT SHIP-BASED AIRCRAFT + + For multiplayer, NoGapGUI must run on the server (only server) + + STRONGLY RECOMMENDED FOR MISSION DESIGNERS: + - Use 'start from ground hot/cold' to be able to control initial aircraft orientation + + To selectively exempt player units from noGap, add a '-ng' to their name. To exclude them from singleplayer only, use '-sp' + Alternatively, use noGap zones (DML only) + + Version History + 1.0.0 - Initial version + +--]]-- + +noGap.standInUnits = {} -- static replacement, if filled; indexed by name +noGap.liveUnits = {} -- live in-game units, checked regularly +noGap.allPlayerUnits = {} -- for update check to get server notification +noGap.noGapZones = {} -- DML only + +function noGap.staticMXFromUnitMX(theGroup, theUnit) + -- enter with MX data blocks + -- build a static object from mx unit data + local theStatic = {} + theStatic.x = theUnit.x + theStatic.y = theUnit.y + theStatic.livery_id = theUnit.livery_id -- if exists + theStatic.heading = theUnit.heading -- may need some attention + theStatic.type = theUnit.type + theStatic.name = theUnit.name -- same as ME unit + theStatic.cty = cfxMX.countryByName[theGroup.name] + return theStatic +end + +function noGap.staticMXFromUnitName(uName) + local theGroup = cfxMX.playerUnit2Group[uName] + local theUnit = cfxMX.playerUnitByName[uName] + if theGroup and theUnit then + return noGap.staticMXFromUnitMX(theGroup, theUnit) + end + trigger.action.outText("+++noG: ERROR: can't find MX data for unit <" .. uName .. ">", 30) +end + +function noGap.isGroundStart(theGroup) + -- look at route + if not theGroup.route then return false end + local route = theGroup.route + local points = route.points + if not points then return false end + local ip = points[1] + if not ip then return false end + local action = ip.action + if action == "Fly Over Point" then return false end + if action == "Turning Point" then return false end + if action == "Landing" then return false end + -- aircraft is on the ground - but is it in water (carrier)? + local u1 = theGroup.units[1] + local sType = land.getSurfaceType(u1) -- has fields x and y + if sType == 3 then return false end + if noGap.verbose then + trigger.action.outText("noG: Player Group <" .. theGroup.name .. "> GROUND BASED: " .. action .. ", land type " .. sType, 30) + end + return true +end + +function noGap.ignoreMXUnit(theUnit) -- DML-only + local p = {x=theUnit.x, y=0, z=theUnit.y} + for idx, theZone in pairs(noGap.noGapZones) do + if theZone.ngIgnore and cfxZones.pointInZone(p, theZone) then + return true + end + -- only single-player: exclude units in spIgnore zones + if (not noGap.isMP) and + theZone.spIgnore and cfxZones.pointInZone(p, theZone) then + return true + end + end + return false +end + +function noGap.createStandInForMXData(group, theUnit) -- group, theUnit are MX data blocks + local sgMatch = theUnit.name:sub(-#noGap.ignoreMe) == noGap.ignoreMe or group.name:sub(-#noGap.ignoreMe) == noGap.ignoreMe + local spMatch = theUnit.name:sub(-#noGap.spIgnore) == noGap.spIgnore or group.name:sub(-#noGap.spIgnore) == noGap.spIgnore + local zoneIgnore = noGap.ignoreMXUnit(theUnit) + local inGameUnit = Unit.getByName(theUnit.name) + if (theUnit.skill == "Client" or theUnit.skill == "Player") + and (not sgMatch) + and (not spMatch) + and (not zoneIgnore) + then + -- remember this unit as one to check regularly + noGap.allPlayerUnits[theUnit.name] = "NG" .. theUnit.name + -- replace this unit with stand-in if not already in game + if inGameUnit and Unit.isExist(inGameUnit) then + -- already exists, do NOT allocate, and erase + -- any lingering data + noGap.standInUnits[theUnit.name] = nil -- forget static + noGap.liveUnits[theUnit.name] = inGameUnit -- remember live + if noGap.verbose then + trigger.action.outText("+++noG: skipped - unit <" .. theUnit.name .. "> of <" .. group.name .. ">", 30) + end + else + -- create a stand-in + -- and remember + local theStaticMX = noGap.staticMXFromUnitMX(group, theUnit) + local theStatic = coalition.addStaticObject(theStaticMX.cty, theStaticMX) + noGap.standInUnits[theUnit.name] = theStatic -- remember me + if noGap.verbose then + trigger.action.outText("+++noG: unit <" .. theUnit.name .. "> of <" .. group.name .. "> nogapped", 30) + end + end + end + +end + +function noGap.fillGaps() + -- turn on. May turn on any time, even during game + -- when we enter, all slots should be emptry + -- and we populate all slots. If slot in use, don't populate + -- with their static representations + -- a 'slot' is a player aircraft + -- iterate all groups that have at least one player and groundstart + -- as filtered by cfxMX + -- we need to access group because that contains start info + for gName, groupData in pairs (cfxMX.playerGroupByName) do + -- check to see if this group is on the ground at parking + -- by looking at the first waypoint + if noGap.isGroundStart(groupData) then + -- this is one of ours! + -- iterate all player units in this group, + -- and replace those units that are player units + local allUnits = groupData.units + for idx, unitData in pairs(allUnits) do + noGap.createStandInForMXData(groupData, unitData) + end + end -- if groundtstart + end +end + +function noGap.turnOff() + if noGap.verbose then + trigger.action.outText("+++noG: Turning OFF", 30) + end + -- remove all stand-ins + for uName, standIn in pairs (noGap.standInUnits) do + StaticObject.destroy(standIn) + end + noGap.standInUnits = {} +end + +function noGap.turnOn() + if noGap.verbose then + trigger.action.outText("+++noG: Turning on", 30) + end + -- populate all empty (non-taken) slots with stand-ins + noGap.fillGaps() +end + +-- +-- event handling +-- +function noGap:onEvent(event) + if not event then return end + if not event.id then return end + if not event.initiator then return end + local theUnit = event.initiator + + if event.id == 15 then -- we act on player unit birth + if (not theUnit.getPlayerName) or (not theUnit:getPlayerName()) then + return + end -- no player unit. + local uName = theUnit:getName() + + if noGap.standInUnits[uName] then + -- remove static + StaticObject.destroy(noGap.standInUnits[uName]) + noGap.standInUnits[uName] = nil + if noGap.verbose then + trigger.action.outText("+++noG: removed static for <" ..uName .. ">, player inbound", 30) + end + end + noGap.liveUnits[uName] = theUnit + -- reset noGapGUI flag, it has done its job. Unit is live + -- we can reset it for next iteration + trigger.action.setUserFlag("NG"..uName, 0) + end +end + +-- +-- update, includes MP client check code +-- +function noGap.update() + -- check every second. + timer.scheduleFunction(noGap.update, {}, timer.getTime() + 1) + + if not noGap.isMP then + local ngDetect = trigger.misc.getUserFlag("noGapGUI") + if ngDetect > 0 then + trigger.action.outText("noGap: MP activated <" .. ngDetect .. ">, will re-init", 30) + noGap.turnOff() + noGap.isMP = true + if noGap.enabled then + noGap.turnOn() + end + return + end + end + + -- check if client signals for on? or off? + if noGap.turnOn and cfxZones.testZoneFlag(noGap, noGap.turnOnFlag, noGap.triggerMethod, "lastTurnOnFlag") -- warning: noGap is NOT a dmlZone, requires cfxZone invocation + then + if not noGap.enabled then + noGap.turnOn() + else + if noGap.verbose then + trigger.action.outText("+++noG: ignored tun ON event, already active", 30) + end + end + noGap.enabled = true + end + + if noGap.turnOff and cfxZones.testZoneFlag(noGap, noGap.turnOffFlag, noGap.triggerMethod, "lastTurnOffFlag") then + if noGap.enabled then + noGap.turnOff() + end + noGap.enabled = false + end + + if not noGap.enabled then return end + + -- check if activeUnit has disappeared an returns to slot + local filtered = {} + for name, theUnit in pairs(noGap.liveUnits) do + if Unit.isExist(theUnit) then + -- unit still alive + filtered[name] = theUnit + else + -- unit disappeared, make static show up in slot + -- no copy to filtered + local theStaticMX = noGap.staticMXFromUnitName(name) + local theStatic = coalition.addStaticObject(theStaticMX.cty, theStaticMX) + noGap.standInUnits[name] = theStatic -- remember me + if noGap.verbose then + trigger.action.outText("+++noG: unit <" .. name .. "> nogapped", 30) + end + end + end + noGap.liveUnits = filtered + + -- check if noGapGUI signals slot interest by player + for name, ngName in pairs (noGap.allPlayerUnits) do + local ngFlag = trigger.misc.getUserFlag(ngName) + if ngFlag > 0 then + if noGap.standInUnits[name] then + -- static needs to be removed, server wants to occupy + StaticObject.destroy(noGap.standInUnits[name]) + noGap.standInUnits[name] = nil + if noGap.verbose then + trigger.action.outText("+++noG: removing static <" .. name .. "> for server request", 30) + end + -- set flag-based timer + if noGap.timeOut > 0 then + trigger.action.setUserFlag(ngName,-noGap.timeOut) + end + end + elseif ngFlag < 0 then + -- timer is running, count up to 0 + ngFlag = ngFlag + 1 + if ngFlag > -1 then + -- timeout. restore static. this may cause if crash if + -- player waited too long without actually slotting in. + ngFlag = 0 + local theStaticMX = noGap.staticMXFromUnitName(name) + local theStatic = coalition.addStaticObject(theStaticMX.cty, theStaticMX) + noGap.standInUnits[name] = theStatic -- remember me + if noGap.verbose then + trigger.action.outText("+++noG: unit <" .. name .. "> restored after timeout", 30) + end + end + trigger.action.setUserFlag(ngName, ngFlag) + end + end +end + +-- +-- read stopGapZone (DML only) +-- +function noGap.createNoGapZone(theZone) + local ng = theZone:getBoolFromZoneProperty("noGap", true) + if ng then theZone.ngIgnore = false else theZone.sgIgnore = true end +end + +function noGap.createNoGapSPZone(theZone) + local sp = theZone:getBoolFromZoneProperty("noGapSP", true) + if sp then theZone.spIgnore = false else theZone.spIgnore = true end +end + +-- +-- Read Config Zone +-- +noGap.name = "noGapConfig" -- cfxZones compatibility here +function noGap.readConfigZone(theZone) + -- currently nothing to do + noGap.verbose = theZone.verbose + noGap.enabled = theZone:getBoolFromZoneProperty("onStart", true) + noGap.timeOut = theZone:getNumberFromZoneProperty("timeOut", 0) -- default to off + if theZone:hasProperty("on?") then + noGap.turnOnFlag = theZone:getStringFromZoneProperty("on?", "*") + noGap.lastTurnOnFlag = trigger.misc.getUserFlag(noGap.turnOnFlag) + end + if theZone:hasProperty("off?") then + noGap.turnOffFlag = theZone:getStringFromZoneProperty("off?", "*") + noGap.lastTurnOffFlag = trigger.misc.getUserFlag(noGap.turnOffFlag) + end + noGap.triggerMethod = theZone:getStringFromZoneProperty( "triggerMethod", "change") + if noGap.verbose then + trigger.action.outText("+++no: config read, verbose = YES", 30) + if noGap.enabled then + trigger.action.outText("+++noG: enabled", 30) + else + trigger.action.outText("+++noG: turned off", 30) + end + end +end + +-- +-- get going +-- +function noGap.start() + if not dcsCommon.libCheck("cfx noGap", + noGap.requiredLibs) + then return false end + + local sgDetect = trigger.misc.getUserFlag("noGapGUI") + noGap.isMP = sgDetect > 0 + + local theZone = cfxZones.getZoneByName("noGapConfig") + if not theZone then + theZone = cfxZones.createSimpleZone("noGapConfig") + end + noGap.readConfigZone(theZone) + + -- collect exclusion zones + local pZones = cfxZones.zonesWithProperty("noGap") + for k, aZone in pairs(pZones) do + noGap.createNoGapZone(aZone) + noGap.noGapZones[aZone.name] = aZone + end + + -- collect single-player exclusion zones + local pZones = cfxZones.zonesWithProperty("noGapSP") + for k, aZone in pairs(pZones) do + noGap.createNoGapSPZone(aZone) + noGap.noGapZones[aZone.name] = aZone + end + + -- fill player slots with static objects + if noGap.enabled then + noGap.fillGaps() + end + + -- connect event handler + world.addEventHandler(noGap) + + -- start update in 10 seconds + timer.scheduleFunction(noGap.update, {}, timer.getTime() + 1) + + -- say hi! + local mp = " (SP - <" .. sgDetect .. ">)" + if sgDetect > 0 then mp = " -- MP GUI Detected (" .. sgDetect .. ")!" end + trigger.action.outText("noGap v" .. noGap.version .. " running" .. mp, 30) + + return true +end + +if not noGap.start() then + trigger.action.outText("+++ aborted noGap v" .. noGap.version .. " -- startup failed", 30) + noGap = nil +end diff --git a/modules/stopGaps.lua b/modules/stopGaps.lua index da5ed21..2e2efc7 100644 --- a/modules/stopGaps.lua +++ b/modules/stopGaps.lua @@ -421,5 +421,4 @@ end if not stopGap.start() then trigger.action.outText("+++ aborted stopGap v" .. stopGap.version .. " -- startup failed", 30) stopGap = nil -end - +end \ No newline at end of file diff --git a/modules/wiper.lua b/modules/wiper.lua index 5a7f29a..52782e7 100644 --- a/modules/wiper.lua +++ b/modules/wiper.lua @@ -1,5 +1,5 @@ wiper = {} -wiper.version = "1.1.0" +wiper.version = "1.2.0" wiper.verbose = false wiper.ups = 1 wiper.requiredLibs = { @@ -11,7 +11,10 @@ wiper.wipers = {} Version History 1.0.0 - Initial Version 1.1.0 - added zone bounds check before wiping - + 1.2.0 - OOP dmlZones + - categories can now be a list + - declutter opetion + - if first category is 'none', zone will not wipe at all but may declutter --]]-- @@ -34,30 +37,47 @@ end -- read zone -- function wiper.createWiperWithZone(theZone) - theZone.triggerWiperFlag = cfxZones.getStringFromZoneProperty(theZone, "wipe?", "*") + theZone.triggerWiperFlag = theZone:getStringFromZoneProperty("wipe?", "*") -- triggerWiperMethod - theZone.triggerWiperMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") - if cfxZones.hasProperty(theZone, "triggerWiperMethod") then - theZone.triggerWiperMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerWiperMethod", "change") + theZone.triggerWiperMethod = theZone:getStringFromZoneProperty("triggerMethod", "change") + if theZone:hasProperty("triggerWiperMethod") then + theZone.triggerWiperMethod = theZone:getStringFromZoneProperty("triggerWiperMethod", "change") end if theZone.triggerWiperFlag then - theZone.lastTriggerWiperValue = cfxZones.getFlagValue(theZone.triggerWiperFlag, theZone) + theZone.lastTriggerWiperValue = theZone:getFlagValue(theZone.triggerWiperFlag) end - local theCat = cfxZones.getStringFromZoneProperty(theZone, "category", "static") - if cfxZones.hasProperty(theZone, "wipeCategory") then - theCat = cfxZones.getStringFromZoneProperty(theZone, "wipeCategory", "static") + local theCat = theZone:getStringFromZoneProperty("category", "none") + if theZone:hasProperty("wipeCategory") then + theCat = theZone:getStringFromZoneProperty("wipeCategory", "none") end if cfxZones.hasProperty(theZone, "wipeCat") then - theCat = cfxZones.getStringFromZoneProperty(theZone, "wipeCat", "static") + theCat = theZone:getStringFromZoneProperty("wipeCat", "none") end + local allCats = {} + if dcsCommon.containsString(theCat, ",") then + allCats = dcsCommon.splitString(theCat, ",") + allCats = dcsCommon.trimArray(allCats) + else + allCats = {dcsCommon.trim(theCat)} + end + -- translate to category for each entry + theZone.wipeCategory = {} + if allCats[1] == "none" then +-- theZone.wipeCategory = {} -- no category to wipe + else + for idx, aCat in pairs (allCats) do + table.insert(theZone.wipeCategory, dcsCommon.string2ObjectCat(aCat)) + end + end +-- theZone.wipeCategory = dcsCommon.string2ObjectCat(theCat) - theZone.wipeCategory = dcsCommon.string2ObjectCat(theCat) + theZone.declutter = theZone:getBoolFromZoneProperty("declutter", false) - if cfxZones.hasProperty(theZone, "wipeNamed") then - theZone.wipeNamed = cfxZones.getStringFromZoneProperty(theZone, "wipeNamed", "") + if theZone:hasProperty("wipeNamed") then + theZone.wipeNamed = theZone:getStringFromZoneProperty("wipeNamed", "") theZone.oWipeNamed = theZone.wipeNamed -- save original -- assemble list of all names to wipe, including wildcard local allNames = {} @@ -78,15 +98,13 @@ function wiper.createWiperWithZone(theZone) end theDict[shortName] = ew if wiper.verbose or theZone.verbose then - trigger.action.outText("+++wpr: dict [".. shortName .."] = " .. dcsCommon.bool2Text(ew),30) + trigger.action.outText("+++wpr: dict [".. shortName .."], '*' = " .. dcsCommon.bool2Text(ew) .. " for <" .. theZone:getName() .. ">",30) end end - theZone.wipeNamed = theDict - end - theZone.wipeInventory = cfxZones.getBoolFromZoneProperty(theZone, "wipeInventory", false) + theZone.wipeInventory = theZone:getBoolFromZoneProperty("wipeInventory", false) if wiper.verbose or theZone.verbose then trigger.action.outText("+++wpr: new wiper zone <".. theZone.name ..">", 30) @@ -166,57 +184,72 @@ function wiper.isTriggered(theZone) } -- set up remaining arguments local cat = theZone.wipeCategory -- Object.Category.STATIC + -- WARNING: as of version 1.2.0 cat is now a TABLE!!! + -- world.searchObjects supports cat tables according to https://wiki.hoggitworld.com/view/DCS_func_searchObjects - -- now call search - world.searchObjects(cat, args, wiper.objectHandler, collector) - if #collector < 1 and (wiper.verbose or theZone.verbose) then - trigger.action.outText("+++wpr: world search returned zero elements for <" .. theZone.name .. "> (cat=" .. theZone.wipeCategory .. ")",30) - end + if #cat > 0 then + -- now call search + world.searchObjects(cat, args, wiper.objectHandler, collector) + if #collector < 1 and (wiper.verbose or theZone.verbose) then + trigger.action.outText("+++wpr: world search returned zero elements for <" .. theZone.name .. "> (cat=<" .. dcsCommon.array2string(theZone.wipeCategory) .. ">)",30) + end - -- wipe'em! - for idx, anObject in pairs(collector) do - local doWipe = true + -- wipe'em! + for idx, anObject in pairs(collector) do + local doWipe = true - -- see if we filter to only named objects - if theZone.wipeNamed then - doWipe = false - local oName = tostring(anObject:getName()) -- prevent number mismatch - for wipeName, beginsWith in pairs(theZone.wipeNamed) do - if beginsWith then - doWipe = doWipe or dcsCommon.stringStartsWith(oName, wipeName) - else - doWipe = doWipe or oName == wipeName + -- see if we filter to only named objects + if theZone.wipeNamed then + doWipe = false + local oName = tostring(anObject:getName()) -- prevent number mismatch + for wipeName, beginsWith in pairs(theZone.wipeNamed) do + if beginsWith then + doWipe = doWipe or dcsCommon.stringStartsWith(oName, wipeName) + else + doWipe = doWipe or oName == wipeName + end + end + + if wiper.verbose or theZone.verbose then + if not doWipe then + trigger.action.outText("+++wpr: <"..oName.."> not removed, name restriction <" .. theZone.oWipeNamed .. "> not met.",30) + end end end - - if wiper.verbose or theZone.verbose then - if not doWipe then - trigger.action.outText("+++wpr: <"..oName.."> not removed, name restriction <" .. theZone.oWipeNamed .. "> not met.",30) + + -- now also filter by position in zone + local uP = anObject:getPoint() + if doWipe and (not cfxZones.isPointInsideZone(uP, theZone)) then + doWipe = false + if wiper.verbose or theZone.verbose then + trigger.action.outText("+++wpr: <" .. anObject:getName() .."> not removed, outside zone <" .. theZone.name .. "> bounds.",30) + end + end + + if doWipe then + if wiper.verbose or theZone.verbose then + trigger.action.outText("+++wpr: wiping " .. anObject:getName(), 30) + end + anObject:destroy() + else + if wiper.verbose or theZone.verbose then + trigger.action.outText("+++wpr: spared object <" .. anObject:getName() .. ">",30) end end end - - -- now also filter by position in zone - local uP = anObject:getPoint() - if doWipe and (not cfxZones.isPointInsideZone(uP, theZone)) then - doWipe = false - if wiper.verbose or theZone.verbose then - trigger.action.outText("+++wpr: <" .. anObject:getName() .."> not removed, outside zone <" .. theZone.name .. "> bounds.",30) - end + else + if theZone.verbose or wiper.verbose then + trigger.action.outText("+++wpr: <" .. theZone:getName() .. "> has no categories to remove, skipping", 30) end - - if doWipe then - if wiper.verbose or theZone.verbose then - trigger.action.outText("+++wpr: wiping " .. anObject:getName(), 30) - end - anObject:destroy() - else - if wiper.verbose or theZone.verbose then - trigger.action.outText("+++wpr: spared object <" .. anObject:getName() .. ">",30) - end - end - end + end + -- declutter pass if requested + if theZone.declutter then + if theZone.verbose or wiper.verbose then + trigger.action.outText("+++wpr: decluttering <" .. theZone:getName() .. ">", 30) + end + theZone:declutterZone() + end end diff --git a/server modules/noGapGUI.lua b/server modules/noGapGUI.lua new file mode 100644 index 0000000..310783d --- /dev/null +++ b/server modules/noGapGUI.lua @@ -0,0 +1,35 @@ +noGapGUI = {} +noGapGUI.version = "1.0.0" +noGapGUI.fVal = 1 -- tell noGap to remove static +noGapGUI.verbose = false +-- +-- Server Plug-In for noGap mission script, only required for server +-- Put into (main DCS save folder)/Scripts/Hooks/ and restart DCS +-- +function noGapGUI.onPlayerTryChangeSlot(playerID, side, slotID) + if not slotID then return end + if slotID == "" then return end + if not DCS.isServer() then return end + if not DCS.isMultiplayer() then return end + + local uName = DCS.getUnitProperty(slotID, DCS.UNIT_NAME) + if not uName then return end + local ngName = "NG" .. uName + -- tell all clients to remove this unit's static if they are deployed + net.dostring_in("server", " trigger.action.setUserFlag(\""..ngName.."\", " .. noGapGUI.fVal .. "); ") + if noGapGUI.verbose then + net.send_chat("+++NG: readying unit <" .. ngName .. "> for slotting") + else + net.log("+++noGapGUI: readying unit <" .. ngName .. "> for slotting") + end +end + +function noGapGUI.onSimulationStart() + net.dostring_in("server", " trigger.action.setUserFlag(\"noGapGUI\", 0); ") + if not DCS.isServer() then return end + if not DCS.isMultiplayer() then return end + net.dostring_in("server", " trigger.action.setUserFlag(\"noGapGUI\", 200); ") -- tells client that MP is active +end + +DCS.setUserCallbacks(noGapGUI) +net.log("noGapGUI v." .. noGapGUI.version .. " started.") \ No newline at end of file diff --git a/tutorial & demo missions/demo - recon mode - reloaded.miz b/tutorial & demo missions/demo - recon mode - reloaded.miz index 93ab49e..c7c5cce 100644 Binary files a/tutorial & demo missions/demo - recon mode - reloaded.miz and b/tutorial & demo missions/demo - recon mode - reloaded.miz differ diff --git a/tutorial & demo missions/demo - viper with a double youu.miz b/tutorial & demo missions/demo - viper with a double youu.miz index e19780e..121222f 100644 Binary files a/tutorial & demo missions/demo - viper with a double youu.miz and b/tutorial & demo missions/demo - viper with a double youu.miz differ