Version 2.3.4

Tickle, json persistence bug for cyclic structures
This commit is contained in:
Christian Franz 2024-10-10 08:30:18 +02:00
parent a2e8ead577
commit a70fadc550
10 changed files with 501 additions and 56 deletions

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,5 @@
cfxMX = {} cfxMX = {}
cfxMX.version = "2.1.0" cfxMX.version = "2.2.0"
cfxMX.verbose = false cfxMX.verbose = false
--[[-- --[[--
Mission data decoder. Access to ME-built mission structures Mission data decoder. Access to ME-built mission structures
@ -18,11 +18,13 @@ cfxMX.verbose = false
- new isDynamicPlayer() - new isDynamicPlayer()
- new isMEPlayer() - new isMEPlayer()
- new isMEPlayerGroup() - new isMEPlayerGroup()
2.2.0 - new groupCatByName[]
--]]-- --]]--
cfxMX.groupNamesByID = {} cfxMX.groupNamesByID = {}
cfxMX.groupIDbyName = {} cfxMX.groupIDbyName = {}
cfxMX.unitIDbyName = {} cfxMX.unitIDbyName = {}
cfxMX.groupCatByName = {}
cfxMX.groupDataByName = {} cfxMX.groupDataByName = {}
cfxMX.groupTypeByName = {} -- category of group: "helicopter", "plane", "ship"... cfxMX.groupTypeByName = {} -- category of group: "helicopter", "plane", "ship"...
cfxMX.groupCoalitionByName = {} cfxMX.groupCoalitionByName = {}
@ -226,7 +228,6 @@ function cfxMX.createCrossReferences()
category = "train" category = "train"
obj_type_name = "train" obj_type_name = "train"
end end
cfxMX.groupTypeByName[aName] = category cfxMX.groupTypeByName[aName] = category
cfxMX.groupNamesByID[aID] = aName cfxMX.groupNamesByID[aID] = aName
cfxMX.groupIDbyName[aName] = aID cfxMX.groupIDbyName[aName] = aID
@ -237,16 +238,22 @@ function cfxMX.createCrossReferences()
-- now make the type-specific xrefs -- now make the type-specific xrefs
if obj_type_name == "helicopter" then if obj_type_name == "helicopter" then
cfxMX.allHeloByName[aName] = group_data cfxMX.allHeloByName[aName] = group_data
cfxMX.groupCatByName[aName] = 1
elseif obj_type_name == "ship" then elseif obj_type_name == "ship" then
cfxMX.allSeaByName[aName] = group_data cfxMX.allSeaByName[aName] = group_data
cfxMX.groupCatByName[aName] = 3
elseif obj_type_name == "plane" then elseif obj_type_name == "plane" then
cfxMX.allFixedByName[aName] = group_data cfxMX.allFixedByName[aName] = group_data
cfxMX.groupCatByName[aName] = 0
elseif obj_type_name == "vehicle" then elseif obj_type_name == "vehicle" then
cfxMX.allGroundByName[aName] = group_data cfxMX.allGroundByName[aName] = group_data
cfxMX.groupCatByName[aName] = 2
elseif obj_type_name == "static" then elseif obj_type_name == "static" then
cfxMX.allStaticByName[aName] = group_data cfxMX.allStaticByName[aName] = group_data
-- cfxMX.groupCatByName[aName] = -1 -- not covered
elseif obj_type_name == "train" then elseif obj_type_name == "train" then
cfxMX.allTrainsByName[aName] = group_data cfxMX.allTrainsByName[aName] = group_data
cfxMX.groupCatByName[aName] = 4
else else
-- should be impossible, but still -- should be impossible, but still
trigger.action.outText("+++MX: <" .. obj_type_name .. "> unknown type for <" .. aName .. ">", 30) trigger.action.outText("+++MX: <" .. obj_type_name .. "> unknown type for <" .. aName .. ">", 30)

View File

@ -953,6 +953,9 @@ function cfxGroundTroops.addGroundTroopsToPool(troops) -- troops MUST be a table
end end
if not troops.orders then troops.orders = "guard" end if not troops.orders then troops.orders = "guard" end
troops.orders = troops.orders:lower() troops.orders = troops.orders:lower()
--trigger.action.outText("Enter adding group <" .. troops.name .. "> with orders=<" .. troops.orders .. "> to pool", 30)
if not troops.moveFormation then troops.moveFormation = "Custom" end if not troops.moveFormation then troops.moveFormation = "Custom" end
troops.reschedule = true -- in case we use scheduled update troops.reschedule = true -- in case we use scheduled update
-- we now add to internal array. this is worked on by all -- we now add to internal array. this is worked on by all

View File

@ -1,5 +1,5 @@
cfxHeloTroops = {} cfxHeloTroops = {}
cfxHeloTroops.version = "3.1.2" cfxHeloTroops.version = "3.1.3"
cfxHeloTroops.verbose = false cfxHeloTroops.verbose = false
cfxHeloTroops.autoDrop = true cfxHeloTroops.autoDrop = true
cfxHeloTroops.autoPickup = false cfxHeloTroops.autoPickup = false
@ -20,6 +20,10 @@ cfxHeloTroops.requestRange = 500 -- meters
3.1.0 - compatible with DCS 2.9.6 dynamic spawning 3.1.0 - compatible with DCS 2.9.6 dynamic spawning
3.1.1 - deployTroopsFromHelicopter() captureandhold 3.1.1 - deployTroopsFromHelicopter() captureandhold
3.1.2 - doLoadGroup - test if group is still alive edge case handling 3.1.2 - doLoadGroup - test if group is still alive edge case handling
3.1.3 - decycled structures (destination zone) on save
- upcycled structures (destination) on load
- loadSound and disembarkSound
--]]-- --]]--
@ -658,7 +662,7 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf)
troop.destination = dest -- transfer target zone for attackzone oders troop.destination = dest -- transfer target zone for attackzone oders
cfxGroundTroops.addGroundTroopsToPool(troop) -- will schedule move orders cfxGroundTroops.addGroundTroopsToPool(troop) -- will schedule move orders
trigger.action.outTextForGroup(conf.id, "<" .. theGroup:getName() .. "> have deployed to the ground with orders " .. orders .. "!", 30) trigger.action.outTextForGroup(conf.id, "<" .. theGroup:getName() .. "> have deployed to the ground with orders " .. orders .. "!", 30)
trigger.action.outSoundForGroup(conf.id, cfxHeloTroops.actionSound) trigger.action.outSoundForGroup(conf.id, cfxHeloTroops.disembarkSound)
-- see if this is tracked by a tracker, and pass them back so -- see if this is tracked by a tracker, and pass them back so
-- they can un-limbo -- they can un-limbo
if groupTracker then if groupTracker then
@ -747,7 +751,7 @@ function cfxHeloTroops.doLoadGroup(args)
-- say so -- say so
trigger.action.outTextForGroup(conf.id, "Team '".. conf.troopsOnBoard.name .."' aboard, ready to go!", 30) trigger.action.outTextForGroup(conf.id, "Team '".. conf.troopsOnBoard.name .."' aboard, ready to go!", 30)
trigger.action.outSoundForGroup(conf.id, cfxHeloTroops.actionSound) -- "Quest Snare 3.wav") trigger.action.outSoundForGroup(conf.id, cfxHeloTroops.loadSound) -- "Quest Snare 3.wav")
-- reset menu -- reset menu
cfxHeloTroops.removeComms(conf.unit) cfxHeloTroops.removeComms(conf.unit)
@ -898,6 +902,8 @@ function cfxHeloTroops.readConfigZone()
cfxHeloTroops.combatDropScore = theZone:getNumberFromZoneProperty( "combatDropScore", 200) cfxHeloTroops.combatDropScore = theZone:getNumberFromZoneProperty( "combatDropScore", 200)
cfxHeloTroops.actionSound = theZone:getStringFromZoneProperty("actionSound", "Quest Snare 3.wav") cfxHeloTroops.actionSound = theZone:getStringFromZoneProperty("actionSound", "Quest Snare 3.wav")
cfxHeloTroops.loadSound = theZone:getStringFromZoneProperty("loadSound", cfxHeloTroops.actionSound)
cfxHeloTroops.disembarkSound = theZone:getStringFromZoneProperty("disembarkSound", cfxHeloTroops.actionSound)
cfxHeloTroops.requestRange = theZone:getNumberFromZoneProperty("requestRange", 500) cfxHeloTroops.requestRange = theZone:getNumberFromZoneProperty("requestRange", 500)
-- add own troop carriers -- add own troop carriers
@ -920,6 +926,10 @@ function cfxHeloTroops.saveData()
for gName, gData in pairs(cfxHeloTroops.deployedTroops) do for gName, gData in pairs(cfxHeloTroops.deployedTroops) do
local sData = dcsCommon.clone(gData) local sData = dcsCommon.clone(gData)
dcsCommon.synchGroupData(sData.groupData) dcsCommon.synchGroupData(sData.groupData)
if sData.destination then
net.log("cfxHeloTroops: decycling troop 'destination' for <" .. sData.destination:getName() .. ">")
sData.destination = sData.destination:getName()
end
allTroopData[gName] = sData allTroopData[gName] = sData
end end
theData.troops = allTroopData theData.troops = allTroopData
@ -947,6 +957,13 @@ function cfxHeloTroops.loadData()
local range = gdTroop.range local range = gdTroop.range
local cty = gData.cty local cty = gData.cty
local cat = gData.cat local cat = gData.cat
local dest = nil
-- synch destination from name to real zone
if gdTroop.destination then
dest = cfxZones.getZoneByName(gdTroop.destination)
net.log("cfxHeloTroops: attempting to restore troop destination zone <" .. gdTroop.destination .. ">")
end
-- now spawn, but first -- now spawn, but first
-- add to my own deployed queue so we can save later -- add to my own deployed queue so we can save later
@ -957,6 +974,7 @@ function cfxHeloTroops.loadData()
-- add to groundTroops -- add to groundTroops
local newTroops = cfxGroundTroops.createGroundTroops(theGroup, range, orders) local newTroops = cfxGroundTroops.createGroundTroops(theGroup, range, orders)
newTroops.destination = dest
cfxGroundTroops.addGroundTroopsToPool(newTroops) cfxGroundTroops.addGroundTroopsToPool(newTroops)
end end
end end

View File

@ -1,6 +1,6 @@
impostors={} impostors={}
impostors.version = "1.0.1" impostors.version = "1.1.0"
impostors.verbose = false impostors.verbose = false
impostors.ups = 1 impostors.ups = 1
impostors.requiredLibs = { impostors.requiredLibs = {
@ -18,7 +18,9 @@ impostors.uniqueCounter = 8200000 -- clones start at 9200000
Version History Version History
1.0.0 - initial version 1.0.0 - initial version
1.0.1 - added some verbosity 1.0.1 - added some verbosity
1.1.0 - filtered dead units during spawns
cleanup
some performance boost for mx lookup
LIMITATIONS: LIMITATIONS:
must be on ground (or would be very silly must be on ground (or would be very silly
@ -41,11 +43,9 @@ function impostors.getCloneZoneByName(aName)
if impostors.verbose then if impostors.verbose then
trigger.action.outText("+++ipst: no impostor with name <" .. aName ..">", 30) trigger.action.outText("+++ipst: no impostor with name <" .. aName ..">", 30)
end end
return nil return nil
end end
-- --
-- spawn impostors from data -- spawn impostors from data
-- --
@ -110,7 +110,9 @@ function impostors.getRawDataFromGroupNamed(gName)
local cat = theGroup:getCategory() local cat = theGroup:getCategory()
-- access mxdata for livery because getDesc does not return the livery -- access mxdata for livery because getDesc does not return the livery
local liveries = {} local liveries = {}
local mxData = cfxMX.getGroupFromDCSbyName(gName) -- local mxData = cfxMX.getGroupFromDCSbyName(gName)
local mxData = cfxMX.groupDataByName[gName] -- performance
if mxData then mxData = dcsCommon.clone(mxData) end
for idx, theUnit in pairs (mxData.units) do for idx, theUnit in pairs (mxData.units) do
liveries[theUnit.name] = theUnit.livery_id liveries[theUnit.name] = theUnit.livery_id
end end
@ -136,8 +138,9 @@ function impostors.getRawDataFromGroupNamed(gName)
ir.y = up.z -- !!! warning! ir.y = up.z -- !!! warning!
-- see if any zones are linked to this unit -- see if any zones are linked to this unit
ir.linkedZones = cfxZones.zonesLinkedToUnit(theUnit) ir.linkedZones = cfxZones.zonesLinkedToUnit(theUnit)
if theUnit:getLife() > 1 then
table.insert(rawUnits, ir) table.insert(rawUnits, ir)
end
ctry = theUnit:getCountry() ctry = theUnit:getCountry()
end end
rawGroup.ctry = ctry rawGroup.ctry = ctry
@ -161,21 +164,15 @@ function impostors.createImpostorWithZone(theZone) -- has "impostor?"
theZone.reanimateFlag = cfxZones.getStringFromZoneProperty(theZone, "reanimate?", "*<none>") theZone.reanimateFlag = cfxZones.getStringFromZoneProperty(theZone, "reanimate?", "*<none>")
theZone.lastReanimateValue = cfxZones.getFlagValue(theZone.reanimateFlag, theZone) theZone.lastReanimateValue = cfxZones.getFlagValue(theZone.reanimateFlag, theZone)
end end
-- watchflag:
-- triggerMethod
theZone.impostorTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") theZone.impostorTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change")
if cfxZones.hasProperty(theZone, "impostorTriggerMethod") then if cfxZones.hasProperty(theZone, "impostorTriggerMethod") then
theZone.impostorTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "impostorTriggerMethod", "change") theZone.impostorTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "impostorTriggerMethod", "change")
end end
-- local localGroups = impostors.allGroupsInZoneByData(theZone)
theZone.groupNames = cfxZones.allGroupNamesInZone(theZone) theZone.groupNames = cfxZones.allGroupNamesInZone(theZone)
theZone.impostor = false -- we have not yet turned units into impostors theZone.impostor = false -- we have not yet turned units into impostors
theZone.myImpostors = {} theZone.myImpostors = {}
theZone.origin = cfxZones.getPoint(theZone) -- save reference point for all groupVectors theZone.origin = cfxZones.getPoint(theZone) -- save reference point for all groupVectors
theZone.onStart = cfxZones.getBoolFromZoneProperty(theZone, "onStart", false) theZone.onStart = cfxZones.getBoolFromZoneProperty(theZone, "onStart", false)
-- blinking -- blinking
@ -207,7 +204,6 @@ function impostors.createImpostorWithZone(theZone) -- has "impostor?"
-- we end with group replaced by impostors -- we end with group replaced by impostors
end end
-- --
-- Spawning -- Spawning
-- --
@ -230,6 +226,9 @@ function impostors.turnGroupsIntoImpostors(theZone)
end end
local aGroup = Group.getByName(gName) local aGroup = Group.getByName(gName)
if aGroup and gName then if aGroup and gName then
if theZone.verbose then
trigger.action.outText("impostoring group <" .. gName .. ">", 30)
end
-- record unit data to create impostors -- record unit data to create impostors
local rawData, cat, ctry = impostors.getRawDataFromGroupNamed(gName) local rawData, cat, ctry = impostors.getRawDataFromGroupNamed(gName)
-- if we are tracking the group, remove it from tracker -- if we are tracking the group, remove it from tracker
@ -241,8 +240,6 @@ function impostors.turnGroupsIntoImpostors(theZone)
-- we may do some book-keeping first for the -- we may do some book-keeping first for the
-- names. we'll see later -- names. we'll see later
Group.destroy(aGroup) Group.destroy(aGroup)
-- local rawData, cat, ctry = cfxMX.getGroupFromDCSbyName(gName)
-- local origID = rawData.groupId -- may be redundant
-- now spawn impostors based on the rawData, -- now spawn impostors based on the rawData,
-- and return impostorGroup -- and return impostorGroup
local impostorGroup = impostors.spawnImpostorsFromData(rawData, cat, ctry) local impostorGroup = impostors.spawnImpostorsFromData(rawData, cat, ctry)
@ -303,12 +300,22 @@ function impostors.spawnGroupsFromImpostor(theZone)
for idx, groupName in pairs(theZone.groupNames) do for idx, groupName in pairs(theZone.groupNames) do
-- get my group data from MX based on my name -- get my group data from MX based on my name
-- we get from MX so we get all path and order info -- we get from MX so we get all path and order info
local rawData, cat, ctry = cfxMX.getGroupFromDCSbyName(groupName) -- local rawData, cat, ctry = cfxMX.getGroupFromDCSbyName(groupName)
local rawData = cfxMX.groupDataByName[groupName]
if rawData then rawData = dcsCommon.clone(rawData) end
local cat = cfxMX.groupCatByName[groupName]
local ctry = cfxMX.countryByName[groupName]
local impostorGroup = theZone.myImpostors[groupName] local impostorGroup = theZone.myImpostors[groupName]
local relinkZones = {} local relinkZones = {}
-- now iterate all units in that group, and remove their impostors -- now iterate all units in that group, and remove their impostors
for idy, theUnit in pairs(rawData.units) do for idy, theUnit in pairs(rawData.units) do
if theUnit and theUnit.name then
local impName = impostorGroup[theUnit.name] local impName = impostorGroup[theUnit.name]
if not impName then
if theZone.verbose then
trigger.action.outText("group <" .. groupName .. ">: no impostor for <" .. theUnit.name .. ">", 30)
end
else
local impStat = StaticObject.getByName(impName) local impStat = StaticObject.getByName(impName)
if impStat and impStat:isExist() and impStat:getLife() > 1 then if impStat and impStat:isExist() and impStat:getLife() > 1 then
-- still alive. read x, y and heading -- still alive. read x, y and heading
@ -327,6 +334,8 @@ function impostors.spawnGroupsFromImpostor(theZone)
impStat:destroy() impStat:destroy()
end end
end end
end
end
-- destroy impostor info -- destroy impostor info
theZone.myImpostors[groupName] = nil theZone.myImpostors[groupName] = nil
@ -335,7 +344,8 @@ function impostors.spawnGroupsFromImpostor(theZone)
-- now create the group -- now create the group
if theZone.blinkTime <= 0 then if theZone.blinkTime <= 0 then
-- immediate spawn -- immediate spawn
local newGroup = coalition.addGroup(ctry, cfxMX.catText2ID(cat), rawData) --local newGroup = coalition.addGroup(ctry, cfxMX.catText2ID(cat), rawData)
local newGroup = coalition.addGroup(ctry, cat, rawData)
impostors.relinkZonesForGroup(relinkZones, newGroup) impostors.relinkZonesForGroup(relinkZones, newGroup)
if theZone.trackWith and groupTracker.addGroupToTrackerNamed then if theZone.trackWith and groupTracker.addGroupToTrackerNamed then
-- add these groups to the group tracker -- add these groups to the group tracker
@ -349,7 +359,7 @@ function impostors.spawnGroupsFromImpostor(theZone)
theZone.blinkCount = theZone.blinkCount + 1 -- so healthcheck avoids false positives theZone.blinkCount = theZone.blinkCount + 1 -- so healthcheck avoids false positives
local args = {} local args = {}
args.ctry = ctry args.ctry = ctry
args.cat = cfxMX.catText2ID(cat) args.cat = cat -- cfxMX.catText2ID(cat)
args.rawData = rawData args.rawData = rawData
args.theZone = theZone args.theZone = theZone
args.relinkZones = relinkZones args.relinkZones = relinkZones
@ -384,12 +394,6 @@ function impostors.delayedSpawn(args)
local newGroup = coalition.addGroup(ctry, cat, rawData) local newGroup = coalition.addGroup(ctry, cat, rawData)
impostors.relinkZonesForGroup(relinkZones, newGroup) impostors.relinkZonesForGroup(relinkZones, newGroup)
if newGroup then
-- trigger.action.outText("+++ipst: SUCCESS!!! Spawned group <" .. newGroup:getName() .. "> for impostor <" .. rawData.name .. ">", 30)
else
trigger.action.outText("+++ipst: failed to spawn group for impostor <" .. rawData.name .. ">", 30)
end
if theZone.trackWith and groupTracker.addGroupToTrackerNamed then if theZone.trackWith and groupTracker.addGroupToTrackerNamed then
-- add these groups to the group tracker -- add these groups to the group tracker
if theZone.verbose or impostors.verbose then if theZone.verbose or impostors.verbose then
@ -409,7 +413,6 @@ function impostors.delayedCleanup(deadUnits)
end end
end end
end end
-- --
-- healthCheck -- healthCheck
-- --
@ -502,8 +505,6 @@ function impostors.update()
end end
end end
end end
-- --
-- start -- start
-- --

View File

@ -1,5 +1,5 @@
persistence = {} persistence = {}
persistence.version = "3.0.1" persistence.version = "3.0.2"
persistence.ups = 1 -- once every 1 seconds persistence.ups = 1 -- once every 1 seconds
persistence.verbose = false persistence.verbose = false
persistence.active = false persistence.active = false
@ -22,6 +22,8 @@ persistence.requiredLibs = {
API cleanup API cleanup
shared text data "flase" typo corrected (no impact) shared text data "flase" typo corrected (no impact)
code cleanup code cleanup
3.0.2 - more logging
vardump to log possible
PROVIDES LOAD/SAVE ABILITY TO MODULES PROVIDES LOAD/SAVE ABILITY TO MODULES
PROVIDES STANDALONE/HOSTED SERVER COMPATIBILITY PROVIDES STANDALONE/HOSTED SERVER COMPATIBILITY
@ -165,12 +167,17 @@ function persistence.saveText(theString, fileName, shared, append)
end end
function persistence.saveTable(theTable, fileName, shared, append) function persistence.saveTable(theTable, fileName, shared, append)
net.log("persistence: enter saveTable")
if not persistence.active then return false end if not persistence.active then return false end
if not fileName then return false end if not fileName then return false end
if not theTable then return false end if not theTable then return false end
if not shared then shared = false end if not shared then shared = false end
net.log("persistence: before json conversion")
local theString = net.lua2json(theTable) local theString = net.lua2json(theTable)
net.log("persistence: json conversion complete")
if not theString then theString = "" end if not theString then theString = "" end
local path = persistence.missionDir .. fileName local path = persistence.missionDir .. fileName
if shared then if shared then
@ -178,6 +185,7 @@ function persistence.saveTable(theTable, fileName, shared, append)
path = persistence.sharedDir .. fileName .. ".txt" path = persistence.sharedDir .. fileName .. ".txt"
end end
net.log("persistence: will now open file at path <" .. path .. ">")
local theFile = nil local theFile = nil
if append then if append then
theFile = io.open(path, "a") theFile = io.open(path, "a")
@ -187,8 +195,11 @@ function persistence.saveTable(theTable, fileName, shared, append)
if not theFile then if not theFile then
return false return false
end end
net.log("persistence: will now write file")
theFile:write(theString) theFile:write(theString)
net.log("persistence: will now close file")
theFile:close() theFile:close()
net.log("persistence: will now exit saveTable")
return true return true
end end
@ -310,6 +321,35 @@ function persistence.missionStartDataLoad()
end end
--
-- logging data
--
function persistence.logTable(key, value, prefix, inrecursion)
local comma = ""
if inrecursion then
if tonumber(key) then key = '[' .. key .. ']' else key = '["' .. key .. '"]' end
comma = ","
end
if not value then value = false end -- not NIL!
if not prefix then prefix = "" else prefix = "\t" .. prefix end
if type(value) == "table" then -- recursively write a table
net.log(prefix .. key .. " = \n" .. prefix .. "{\n")
for k,v in pairs (value) do -- iterate all kvp
persistence.logTable(k, v, prefix, true)
end
net.log(prefix .. "}" .. comma .. " -- end of " .. key .. "\n")
elseif type(value) == "boolean" then
local b = "false"
if value then b = "true" end
net.log(prefix .. key .. " = " .. b .. comma .. "\n")
elseif type(value) == "string" then -- quoted string, WITH proccing
value = string.gsub(value, "\\", "\\\\") -- escape "\" to "\\", others ignored, possibly conflict with \n
value = string.gsub(value, string.char(10), "\\" .. string.char(10)) -- 0A --> "\"0A
net.log(prefix .. key .. ' = "' .. value .. '"' .. comma .. "\n")
else -- simple var, show contents, ends recursion
net.log(prefix .. key .. " = " .. value .. comma .. "\n")
end
end
-- --
-- MAIN DATA WRITE -- MAIN DATA WRITE
-- --
@ -342,9 +382,10 @@ function persistence.saveMissionData()
-- now handle flags -- now handle flags
myData["persistence.flagData"] = persistence.collectFlagData() myData["persistence.flagData"] = persistence.collectFlagData()
net.log("persistence: --- START of module-individual save")
-- now handle all other modules -- now handle all other modules
for moduleName, callbacks in pairs(persistence.callbacks) do for moduleName, callbacks in pairs(persistence.callbacks) do
net.log("persistence: invoking save for module " .. moduleName)
local moduleData, sharedName = callbacks.persistData() local moduleData, sharedName = callbacks.persistData()
if moduleData then if moduleData then
if sharedName then -- save into shared bucket if sharedName then -- save into shared bucket
@ -358,18 +399,30 @@ function persistence.saveMissionData()
if persistence.verbose then if persistence.verbose then
trigger.action.outText("+++persistence: gathered data from <" .. moduleName .. ">", 30) trigger.action.outText("+++persistence: gathered data from <" .. moduleName .. ">", 30)
end end
net.log("persistence: got data for module: " .. moduleName)
--persistence.logTable(moduleName, moduleData)
--net.log("persistence: performing json conversion test for myData")
--local theString = net.lua2json(myData)
--net.log("persistence: json conversion success!")
else else
if persistence.verbose then if persistence.verbose then
trigger.action.outText("+++persistence: NO DATA gathered data from <" .. moduleName .. ">, module returned NIL", 30) trigger.action.outText("+++persistence: NO DATA gathered data from <" .. moduleName .. ">, module returned NIL", 30)
end end
end end
net.log("persistence: completed save for module " .. moduleName)
end end
net.log("persistence: --- END of module-individual save")
-- now save data to file -- now save data to file
net.log("persistence: will now invoke main saveTable")
persistence.saveTable(myData, persistence.saveFileName) persistence.saveTable(myData, persistence.saveFileName)
net.log("persistence: returned from main save table")
-- now save all shared name data as separate files -- now save all shared name data as separate files
net.log("persistence: will now iterate shares")
for shareName, data in pairs (allSharedData) do for shareName, data in pairs (allSharedData) do
net.log("persistence: share " .. shareName)
-- save into shared folder, by name that was returned from callback -- save into shared folder, by name that was returned from callback
-- read what was saved, and replace changed key/values from data -- read what was saved, and replace changed key/values from data
local shFile = persistence.sharedDir .. shareName .. ".txt" local shFile = persistence.sharedDir .. shareName .. ".txt"
@ -384,12 +437,15 @@ function persistence.saveMissionData()
persistence.saveTable(theData, shareName, true) -- true --> shared persistence.saveTable(theData, shareName, true) -- true --> shared
end end
net.log("persistence: done iterating shares")
end end
-- --
-- UPDATE -- UPDATE
-- --
function persistence.doSaveMission() function persistence.doSaveMission()
net.log("persistence: start doSaveMission")
-- main save entry, also from API -- main save entry, also from API
if persistence.verbose then if persistence.verbose then
trigger.action.outText("+++persistence: starting save", 30) trigger.action.outText("+++persistence: starting save", 30)
@ -407,6 +463,7 @@ function persistence.doSaveMission()
if persistence.saveNotification then if persistence.saveNotification then
trigger.action.outText("+++persistence: mission saved to\n" .. persistence.missionDir .. persistence.saveFileName, 30) trigger.action.outText("+++persistence: mission saved to\n" .. persistence.missionDir .. persistence.saveFileName, 30)
end end
net.log("persistence: DONE doSaveMission")
end end
function persistence.noteCleanRestart() function persistence.noteCleanRestart()

View File

@ -18,6 +18,7 @@ cfxPlayerScore.firstSave = true -- to force overwrite
3.3.0 - case INsensitivity for all typeScore objects 3.3.0 - case INsensitivity for all typeScore objects
3.3.1 - fixes for DCS oddity in events after update 3.3.1 - fixes for DCS oddity in events after update
- cleanup - cleanup
TODO: Kill event no longer invoked for map objetcs, attribute TODO: Kill event no longer invoked for map objetcs, attribute
to faction now, reverse invocation direction with PlayerScore to faction now, reverse invocation direction with PlayerScore
--]]-- --]]--

View File

@ -27,6 +27,7 @@ function sweeper.readSweeperZone(theZone)
end end
function sweeper.update() function sweeper.update()
net.log("sweeper: begin update")
timer.scheduleFunction(sweeper.update, {}, timer.getTime() + sweeper.interval) timer.scheduleFunction(sweeper.update, {}, timer.getTime() + sweeper.interval)
local toKill = {} local toKill = {}
local newFlights = {} local newFlights = {}
@ -105,6 +106,8 @@ function sweeper.update()
-- remember new list, forget old -- remember new list, forget old
sweeper.flights = newFlights sweeper.flights = newFlights
net.log("sweeper: end update")
end end
function sweeper.readConfig() function sweeper.readConfig()

355
server modules/tcliGUI.lua Normal file
View File

@ -0,0 +1,355 @@
tcli = {}
tcli.version = "1.1.0"
--[[--
Tickle - a tiny DCS admin CLI (c) 2024 by Christian "CFrag" Franz
Version History
1.1.0 - endTime init to very, very late
- more mission begin load logging
- stronger guards for onXXX
- "-shuffle" and "-sequence" commands
- better -? response
- drove sample time up to 50 seconds between scans
- "-cycle" command
--]]--
tcli.myConfig = lfs.writedir() .. "Missions\\" .. "tcli.config"
tcli.serverCfgPath = lfs.writedir() .. "Config\\" .. "serverSettings.lua"
tcli.lastTime = -1
local config = {}
config.mark = "-"
config.admins = {} -- who is allowed to command
config.cycleTime = -1 -- no cycles, auto miz change off
table.insert(config.admins, "xk76hkl@01") -- some silly user names.
table.insert(config.admins, "%tgJsgRG1<") -- change to your own in the config file that is created in Missions AFTER DCS starts up
tcli.config = config
-- utils
function tcli.hasFile(path) --check if file exists at path
local attr = lfs.attributes(path)
if attr then return true, attr.mode end
return false
end
function tcli.loadFile(path)
if not path then return nil end
local theFile = io.open(path, "r")
if not theFile then return nil end
local t = theFile:read("*a")
theFile:close()
return t
end
function tcli.loadData(path) -- load file as lua table, full path in fileName
local t = tcli.loadFile(path)
if not t then return nil end
local d = net.json2lua(t)
return d
end
function tcli.saveData(path, theData) -- save theData (table) as json text file
if not theData then return false end
local theString = net.lua2json(theData)
if not theString then theString = "" end
local theFile = nil
theFile = io.open(path, "w") -- overwrite
if not theFile then return false end
theFile:write(theString)
theFile:close()
return true
end
function tcli.nameInTable(name, T)
for idx, aName in pairs(T) do
if name == aName then return true end
end
return false
end
-- save a lua table to file
function tcli.saveLuaTable(path, theTable, theName)
local theFile = nil
theFile = io.open(path, "w") -- overwrite
tcli.writeTable(theFile, theName, theTable)
theFile:write("\n-- tickled by tcli")
theFile:close()
end
function tcli.writeTable(theFile, key, value, prefix, inrecursion)
local comma = ""
if inrecursion then
if tonumber(key) then key = '[' .. key .. ']' else key = '["' .. key .. '"]' end
comma = ","
end
if not value then value = false end -- not NIL!
if not prefix then prefix = "" else prefix = "\t" .. prefix end
if type(value) == "table" then -- recursively write a table
theFile:write(prefix .. key .. " = \n" .. prefix .. "{\n")
for k,v in pairs (value) do -- iterate all kvp
tcli.writeTable(theFile, k, v, prefix, true)
end
theFile:write(prefix .. "}" .. comma .. " -- end of " .. key .. "\n")
elseif type(value) == "boolean" then
local b = "false"
if value then b = "true" end
theFile:write(prefix .. key .. " = " .. b .. comma .. "\n")
elseif type(value) == "string" then -- quoted string, WITH proccing
value = string.gsub(value, "\\", "\\\\") -- escape "\" to "\\", others ignored, possibly conflict with \n
value = string.gsub(value, string.char(10), "\\" .. string.char(10)) -- 0A --> "\"0A
theFile:write(prefix .. key .. ' = "' .. value .. '"' .. comma .. "\n")
else -- simple var, show contents, ends recursion
theFile:write(prefix .. key .. " = " .. value .. comma .. "\n")
end
end
-- CLI for admins --
function tcli.adminCall(playerID, line) -- returns string
-- break line into space-delimited commands
local cmd = {}
local sep = " "
for str in string.gmatch(line, "([^"..sep.."]+)") do
table.insert(cmd, str)
end
if #cmd < 1 then return "adm: input error" end
local c = cmd[1]
if c then c = string.upper(c) end
if c == "?" then return tcli.help() end
if c == "NEXT" then return tcli.nextMission() end
if c == "PREV" or c == "PREVIOUS" then return tcli.previousMission() end
if c == "RANDOM" then return tcli.randomMission() end
if c == "RESTART" then return tcli.restartMission() end
if c == "PAUSE" then return tcli.pauseMission(true) end
if c == "PLAY" then return tcli.pauseMission(false) end
if c == "CYCLETIME" then return tcli.cycleTime(cmd[2]) end
if c == "SHUFFLE" then return tcli.shuffle() end
if c == "SEQ" or c == "SEQUENCE" then return tcli.unshuffle() end
if c == "CYCLE" then return tcli.cycleNow() end
return "cli: unknown command <" .. c .. ">"
end
function tcli.help()
local cycleStatus = "Disabled automatic mission change"
if not tcli.config.cycleTime then tcli.config.cycleTime = -1 end
if tcli.config.cycleTime >= 1 then
local now = DCS.getModelTime()
if not tcli.endTime then tcli.endTime = 99999999 end
local remains = tcli.endTime - now
cycleStatus = "AUTOMATIC MISSION CHANGE EVERY " .. tcli.config.cycleTime .. " MINUTES, " .. tcli.num2ms(remains) .. " MMM:SS remaining"
end
if tcli.cfg.listShuffle then cycleStatus = cycleStatus .. ", shuffle ON" else cycleStatus = cycleStatus .. ", NO shuffle" end
local s = "CLI v" .. tcli.version .. ": -? (help), -next, -previous, -restart, -random, -cycle, -pause, -play, -cycleTime, -shuffle, -sequence"
s = s .. " " .. cycleStatus
return s
end
function tcli.getMsnIndex()
local curr = DCS.getMissionFilename( ) -- gets full path
for x, msn in pairs(tcli.cfg.missionList) do
if msn == curr then return x end
end
return 1
end
function tcli.nextMission() -- automatically loops if on last
if #tcli.cfg.missionList < 2 then
return xli.restartMission()
end
local idx = tcli.getMsnIndex() + 1
if idx > #tcli.cfg.missionList then idx = 1 end
local new = tcli.cfg.missionList[idx]
tcli.cfg.listStartIndex = idx
tcli.cfg.lastSelectedMission = new
tcli.saveLuaTable(tcli.serverCfgPath, tcli.cfg, "cfg")
net.log("tcli: Starting next mission in sequence: <" .. new .. ">.")
net.load_mission(new)
return "Loading next mission (" .. new .. ")."
end
function tcli.previousMission() -- automatically loops if on first
if #tcli.cfg.missionList < 2 then
return xli.restartMission()
end
local idx = tcli.getMsnIndex() - 1
if idx < 1 then idx = #tcli.cfg.missionList end
local new = tcli.cfg.missionList[idx]
tcli.cfg.listStartIndex = idx
tcli.cfg.lastSelectedMission = new
tcli.saveLuaTable(tcli.serverCfgPath, tcli.cfg, "cfg")
net.log("tcli: Starting previous mission in sequence: <" .. new .. ">.")
net.load_mission(new)
return "Loading previous mission (" .. new .. ")."
end
function tcli.restartMission()
local curr = DCS.getMissionFilename()
net.load_mission(curr)
return "re-starting mission (" .. curr .. ")"
end
function tcli.randomMission()
if #tcli.cfg.missionList < 2 then return xli.restartMission() end
local curr = DCS.getMissionFilename() -- gets full path
local count = 0
local new
local pick
repeat
pick = math.random(1, #tcli.cfg.missionList)
new = tcli.cfg.missionList[pick]
count = count + 1
until (count > 20) or (new ~= curr)
if count > 20 then return "mission picker error" end
tcli.cfg.listStartIndex = pick
tcli.cfg.lastSelectedMission = new
tcli.saveLuaTable(tcli.serverCfgPath, tcli.cfg, "cfg")
net.log("tcli: Starting random mission: <" .. new .. ">.")
net.load_mission(new)
return "Starting random mission: " .. new
end
function tcli.pauseMission(doPause)
DCS.setPause(doPause)
if doPause then return "Pausing Mission" end
return "Mission continues"
end
function tcli.num2ms(num)
mins = math.floor(num / 60)
sec = math.floor(num%60)
return string.format("%03d", mins) .. ":" .. string.format("%02d", sec)
end
function tcli.cycleTime(param)
if not param then
if tcli.config.cycleTime and tcli.config.cycleTime >= 1 then
local now = DCS.getModelTime()
local remains = tcli.endTime - now
return "AUTOMATIC MISSION CHANGE AFTER " .. tcli.config.cycleTime .. " MINUTES -- now scheduled in " .. tcli.num2ms(remains) .. " MMM:SS"
end
return "DIASBLED automatic mission change"
end
local num = tonumber(param)
if not num then num = -1 end
tcli.config.cycleTime = num
tcli.saveData(tcli.myConfig, tcli.config)
if num >= 1 then tcli.endTime = tcli.config.cycleTime * 60
else tcli.endTime = 0 end
if num >= 1 then return "ENABLED automatic mission change after " .. tcli.config.cycleTime .. " minutes" end
return "Turned OFF automatic mission change"
end
function tcli.shuffle()
tcli.cfg.listShuffle = true
tcli.saveLuaTable(tcli.serverCfgPath, tcli.cfg, "cfg")
return "Mission order is now randomized (shuffled)"
end
function tcli.unshuffle()
tcli.cfg.listShuffle = false
tcli.saveLuaTable(tcli.serverCfgPath, tcli.cfg, "cfg")
return "Missions now play in sequence"
end
function tcli.cycleNow()
if tcli.cfg.listShuffle then -- randomized playlist
tcli.randomMission()
return "Immediately cyling to random mission."
end -- next, will loop
tcli.nextMission()
return "Immediately cycling to next mission."
end
--
-- CLI MAIN ENTRY, command in message
--
function tcli.onPlayerTrySendChat(playerID, message, all )
if not DCS.isServer() then return end
if not DCS.isMultiplayer() then return end
local name = net.get_player_info(playerID, 'name')
-- check to see if message starts with cli mark
local i, j = string.find(message, tcli.config.mark, 1, true)
if i == 1 then -- line starts with cli prompt
if tcli.nameInTable(name, tcli.config.admins) then
message = message:sub(1 + #tcli.config.mark)
local msg = tcli.adminCall(playerID, message)
net.send_chat_to(msg, playerID) -- player only
return "" -- while line output
end
end
end
function tcli.getServerConfig()
-- load/update server config into cfg
local s = tcli.loadFile(tcli.serverCfgPath)
net.log("tcli: loaded server config file: " .. s)
cfg = nil -- nil before loadString
f = loadstring(s)
f() -- define conf so we have access to serverconfig
if cfg then tcli.cfg = cfg end
end
function tcli.onMissionLoadBegin() -- reload to avoid DCS restart
if not DCS.isServer() then return end
if not DCS.isMultiplayer() then return end
tcli.getServerConfig() -- update current list of missions
tcli.lastTime = 0
tcli.hasWarned5 = false
tcli.hasWarned1 = false
tcli.endTime = 99999999 -- very, very late
local d = tcli.loadData(tcli.myConfig) -- update config
if d then tcli.config = d end
if tcli.config.cycleTime and tcli.config.cycleTime >= 1 then
tcli.endTime = tcli.config.cycleTime * 60 -- in minutes!
end
net.log("tcli: Mission <" .. DCS.getMissionName() .. ">: Mission Load Begin - Inited tcli.endTime to <" .. tcli.endTime .. ">.")
end
function tcli.update()
local now = DCS.getModelTime()
-- if cycle time is enabled, we check if we need to advance the Mission
if tcli.config.cycleTime and tcli.config.cycleTime >= 1 then
local remains = tcli.endTime - now
-- warning broadcasts
if not tcli.hasWarned5 and remains < 5 * 60 then
net.send_chat("THIS MISSION ENDS IN 5 MINUTES", true)
tcli.hasWarned5 = true
end
if not tcli.hasWarned1 and remains < 60 then
net.send_chat("THIS MISSION ENDS IN 1 MINUTE", true)
tcli.hasWarned1 = true
end
if remains < 0 then
if tcli.cfg.listShuffle then -- randomized playlist
tcli.randomMission()
else -- next, will loop
tcli.nextMission()
end
end
end
end
function tcli.onSimulationFrame()
if not DCS.isServer() then return end
if not DCS.isMultiplayer() then return end
-- every 50 seconds we do an update. not during pause!
if tcli.lastTime + 50 < DCS.getModelTime() then
tcli.update()
tcli.lastTime = DCS.getModelTime()
end
end
-- start up
if tcli.hasFile(tcli.myConfig) then
local d = tcli.loadData(tcli.myConfig)
if d then
tcli.config = d
net.log("tcli: successfuly read existing config file")
else
net.log("tcli: ERROR LOADING CONFIG FILE. DELETE AND TRY AGAIN.")
end
else
tcli.saveData(tcli.myConfig, tcli.config)
net.log("tcli: created new tcli config file.")
end
-- hook into dcs server
DCS.setUserCallbacks(tcli)