persistence = {} persistence.version = "3.0.0" persistence.ups = 1 -- once every 1 seconds persistence.verbose = false persistence.active = false persistence.saveFileName = nil -- "mission data.txt" persistence.sharedDir = nil -- not yet implemented persistence.missionDir = nil -- set at start persistence.saveDir = nil -- set at start persistence.name = "persistence" -- for cfxZones persistence.missionData = {} -- loaded from file persistence.requiredLibs = { "dcsCommon", "cfxZones", } --[[-- Version History 2.0.0 - dml zones, OOP cleanup 3.0.0 - shared data PROVIDES LOAD/SAVE ABILITY TO MODULES PROVIDES STANDALONE/HOSTED SERVER COMPATIBILITY --]]-- -- in order to work, HOST MUST DESANITIZE lfs and io -- -- flags to save. can be added to by saveFlags attribute -- persistence.flagsToSave = {} -- simple table persistence.callbacks = {} -- cbblocks, dictionary by name -- -- modules register here -- function persistence.registerModule(name, callbacks) -- callbacks is a table with the following entries -- callbacks.persistData - method that returns a table -- note that name is also what the data is saved under -- and must be the one given when you retrieve it later persistence.callbacks[name] = callbacks if persistence.verbose then trigger.action.outText("+++persistence: module <" .. name .. "> registered itself", 30) end end function persistence.registerFlagsToSave(flagNames, theZone) -- name can be single flag name or anything that -- a zone definition has to offer, including local -- flags. -- flags can be passed like this: "a, 4-19, 99, kills, *lcl" -- if you pass a local flag, you must pass the zone -- or "persisTEMP" will be used if not theZone then theZone = cfxZones.createSimpleZone("persisTEMP") end local newFlags = dcsCommon.flagArrayFromString(flagNames, persistence.verbose) -- mow process all new flags and add them to the list of flags -- to save for idx, flagName in pairs(newFlags) do if dcsCommon.stringStartsWith(flagName, "*") then flagName = theZone.name .. flagName end table.insert(persistence.flagsToSave, flagName) end end -- -- registered modules call this to get their data -- function persistence.getSavedDataForModule(name, sharedDataName) if not persistence.active then return nil end if not persistence.hasData then return nil end if not persistence.missionData then return end if not sharedDataName then sharedDataName = nil end if sharedDataName then -- we read from shared data and only revert to -- common if we find nothing local shFile = persistence.sharedDir .. sharedDataName .. ".txt" if persistence.verbose then trigger.action.outText("persistence: will try to load shared data from <" .. shFile .. ">", 30) end local theData = persistence.loadTable(shFile, true) if theData then if theData[name] then return theData[name] end if persistence.verbose then trigger.action.outText("persistence: shared data file <" .. sharedDataName .. "> exists but currently holds no data for <" .. name .. ">, reverting to main", 30) end else if persistence.verbose then trigger.action.outText("persistence: shared data file <" .. sharedDataName .. "> does not yet exist, reverting to main", 30) end end end return persistence.missionData[name] -- simply get the modules data block end -- -- Shared Data API -- function persistence.getSharedDataFor(name, item) -- not yet finalized end function persistence.putSharedDataFor(data, name, item) -- not yet finalized end -- -- helper meths -- function persistence.hasFile(path) --check if file exists at path -- will also return true for a directory, follow up with isDir local attr = lfs.attributes(path) if attr then return true, attr.mode end if persistence.verbose then trigger.action.outText("isFile: attributes not found for <" .. path .. ">", 30) end return false, "" end function persistence.isDir(path) -- check if path is a directory local success, mode = persistence.hasFile(path) if success then success = (mode == "directory") end return success end -- -- Main save meths -- function persistence.saveText(theString, fileName, shared, append) if not persistence.active then return false end if not fileName then trigger.action.outText("+++persistence: saveText without fileName", 30) return false end if not shared then shared = flase end if not theString then theString = "" end local path = persistence.missionDir .. fileName if shared then -- we would now change the path trigger.action.outText("+++persistence: NYI: shared", 30) return end local theFile = nil if append then theFile = io.open(path, "a") else theFile = io.open(path, "w") end if not theFile then trigger.action.outText("+++persistence: saveText - unable to open " .. path, 30) return false end theFile:write(theString) theFile:close() return true end function persistence.saveTable(theTable, fileName, shared, append) if not persistence.active then return false end if not fileName then return false end if not theTable then return false end if not shared then shared = false end local theString = net.lua2json(theTable) if not theString then theString = "" end local path = persistence.missionDir .. fileName if shared then -- we change the path to shared path = persistence.sharedDir .. fileName .. ".txt" end local theFile = nil if append then theFile = io.open(path, "a") else theFile = io.open(path, "w") end if not theFile then return false end theFile:write(theString) theFile:close() return true end function persistence.loadText(fileName, hasPath) -- load file as text if not persistence.active then return nil end if not fileName then return nil end local path if hasPath then path = fileName else path = persistence.missionDir .. fileName end if persistence.verbose then trigger.action.outText("persistence: will load text file <" .. path .. ">", 30) 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 persistence.loadTable(fileName, hasPath) -- load file as table if not persistence.active then return nil end if not fileName then return nil end if not hasPath then hasPath = false end local t = persistence.loadText(fileName, hasPath) if not t then return nil end local tab = net.json2lua(t) return tab end -- -- Data Load on Start -- function persistence.initFlagsFromData(theFlags) -- assumes that theFlags is a dictionary containing -- flag names local flagLog = "" local flagCount = 0 for flagName, value in pairs(theFlags) do local val = tonumber(value) -- ensure number if not val then val = 0 end trigger.action.setUserFlag(flagName, val) if flagLog ~= "" then flagLog = flagLog .. ", " .. flagName .. "=" .. val else flagLog = flagName .. "=" .. val end flagCount = flagCount + 1 end if persistence.verbose and flagCount > 0 then trigger.action.outText("+++persistence: loaded " .. flagCount .. " flags from storage:\n" .. flagLog .. "", 30) elseif persistence.verbose then trigger.action.outText("+++persistence: no flags loaded, commencing mission data load", 30) end end function persistence.freshStart() persistence.missionData = {} persistence.hasData = true trigger.action.setUserFlag("cfxPersistenceHasData", 1) end function persistence.missionStartDataLoad() -- check one: see if we have mission data local theData = persistence.loadTable(persistence.saveFileName) if not theData then if persistence.verbose then trigger.action.outText("+++persistence: no saved data, fresh start.", 30) end persistence.freshStart() return end -- there was no data to load if theData["freshMaker"] then if persistence.verbose then trigger.action.outText("+++persistence: detected fresh start.", 30) end persistence.freshStart() return end -- when we get here, we got at least some data. check it if theData["versionID"] or persistence.versionID then local vid = theData.versionID -- note: either may be nil! if vid ~= persistence.versionID then -- we pretend load never happened. -- simply return if persistence.verbose then local curvid = persistence.versionID if not curvid then curvid = "" end if not vid then vid = "" end trigger.action.outText("+++persistence: version mismatch\n(saved = <" .. vid .. "> vs current = <" .. curvid .. ">) - fresh start.", 30) end return end end -- we have valid data, and modules, after signing up -- can init from by data persistence.missionData = theData persistence.hasData = true trigger.action.setUserFlag("cfxPersistenceHasData", 1) -- init my flags from last save local theFlags = theData["persistence.flagData"] if theFlags then persistence.initFlagsFromData(theFlags) end -- we are done for now. modules check in -- after persistence and load their own data -- when they detect that there is data to load trigger.action.outText("+++persistence: successfully read mission save data", 30) end -- -- MAIN DATA WRITE -- function persistence.collectFlagData() local flagData = {} for idx, flagName in pairs (persistence.flagsToSave) do local theNum = trigger.misc.getUserFlag(flagName) flagData[flagName] = theNum end return flagData end function persistence.saveSharedData() trigger.action.outText("WARNING: Persistence's saveSharedData invoked!", 30) end function persistence.saveMissionData() local myData = {} local allSharedData = {} -- organized by 'shared' name returned -- first, handle versionID and freshMaker if persistence.freshMaker then myData["freshMaker"] = true end if persistence.versionID then myData["versionID"] = persistence.versionID end -- now handle flags myData["persistence.flagData"] = persistence.collectFlagData() -- now handle all other modules for moduleName, callbacks in pairs(persistence.callbacks) do local moduleData, sharedName = callbacks.persistData() if moduleData then if sharedName then -- save into shared bucket -- allshared[specificShared[moduleName]] local specificShared = allSharedData[sharedName] if not specificShared then specificShared = {} end specificShared[moduleName] = moduleData allSharedData[sharedName] = specificShared -- write back end myData[moduleName] = moduleData 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 -- now save data to file persistence.saveTable(myData, persistence.saveFileName) -- now save all shared name data as separate files for shareName, data in pairs (allSharedData) do -- save into shared folder, by name that was returned from callback -- read what was saved, and replace changed key/values from data local shFile = persistence.sharedDir .. shareName .. ".txt" local theData = persistence.loadTable(shFile, true) -- hasPath if theData then for k, v in pairs(data) do theData[k] = v end else theData = data end persistence.saveTable(theData, shareName, true) -- true --> shared end end -- -- UPDATE -- function persistence.doSaveMission() -- main save entry, also from API if persistence.verbose then trigger.action.outText("+++persistence: starting save", 30) end if persistence.active then persistence.saveMissionData() else if persistence.verbose then trigger.action.outText("+++persistence: not actice. skipping save", 30) end return end if persistence.saveNotification then trigger.action.outText("+++persistence: mission saved to\n" .. persistence.missionDir .. persistence.saveFileName, 30) end end function persistence.noteCleanRestart() persistence.freshMaker = true persistence.doSaveMission() trigger.action.outText("\n\nYou can re-start the mission for a fresh start.\n\n",30) end function persistence.update() -- call me in a second to poll triggers timer.scheduleFunction(persistence.update, {}, timer.getTime() + 1/persistence.ups) -- check my trigger flag if persistence.saveMission and cfxZones.testZoneFlag(persistence, persistence.saveMission, "change", "lastSaveMission") then persistence.doSaveMission() end if persistence.cleanRestart and cfxZones.testZoneFlag(persistence, persistence.cleanRestart, "change", "lastCleanRestart") then persistence.noteCleanRestart() end -- check my timer if persistence.saveTime and persistence.saveTime < timer.getTime() then persistence.doSaveMission() -- start next cycle persistence.saveTime = persistence.saveInterval * 60 + timer.getTime() end end -- -- config & start -- function persistence.collectFlagsFromZone(theZone) local theFlags = theZone:getStringFromZoneProperty("saveFlags", "*dummy") persistence.registerFlagsToSave(theFlags, theZone) end function persistence.readConfigZone() if not _G["lfs"] then trigger.action.outText("+++persistence: DCS currently not 'desanitized'. Persistence disabled", 30) return end local theZone = cfxZones.getZoneByName("persistenceConfig") local hasConfig = true if not theZone then hasConfig = false theZone = cfxZones.createSimpleZone("persistenceConfig") end -- serverDir is the path from the server save directory, usually "Missions/". -- will be added to lfs.writedir() unless given a root attribute if theZone:hasProperty("root") then -- we split this to enable further processing down the -- line if neccessary persistence.root = theZone:getStringFromZoneProperty("root", lfs.writedir()) -- safe default if not dcsCommon.stringEndsWith(persistence.root, "\\") then persistence.root = persistence.root .. "\\" end if theZone.verbose then trigger.action.outText("+++persistence: setting root to <" .. persistence.root .. ">", 30) end else persistence.root = lfs.writedir() -- safe defaulting if theZone.verbose then trigger.action.outText("+++persistence: defaulting root to <" .. persistence.root .. ">", 30) end end persistence.serverDir = theZone:getStringFromZoneProperty("serverDir", "Missions\\") persistence.sharedDir = "DML-Shared-Data\\" -- hard-wired! if hasConfig then if theZone:hasProperty("saveDir") then persistence.saveDir = theZone:getStringFromZoneProperty("saveDir", "") else -- local missname = net.dostring_in("gui", "return DCS.getMissionName()") .. " (data)" persistence.saveDir = dcsCommon.getMissionName() .. " (data)" end else persistence.saveDir = "" -- save dir is to main mission -- so that when no config is present (standalone debugger) -- this will not cause a separate save folder end if persistence.saveDir == "" and persistence.verbose then trigger.action.outText("*** WARNING: persistence is set to write to main mission directory!", 30) end if theZone:hasProperty("saveFileName") then persistence.saveFileName = theZone:getStringFromZoneProperty("saveFileName", dcsCommon.getMissionName() .. " Data.txt") end if theZone:hasProperty("versionID") then persistence.versionID = theZone:getStringFromZoneProperty("versionID", "") -- to check for full restart end persistence.saveInterval = theZone:getNumberFromZoneProperty("saveInterval", -1) -- default to manual save if persistence.saveInterval > 0 then persistence.saveTime = persistence.saveInterval * 60 + timer.getTime() end if theZone:hasProperty("cleanRestart?") then persistence.cleanRestart = theZone:getStringFromZoneProperty("cleanRestart?", "*") persistence.lastCleanRestart = theZone:getFlagValue(persistence.cleanRestart) end if theZone:hasProperty("saveMission?") then persistence.saveMission = theZone:getStringFromZoneProperty("saveMission?", "*") persistence.lastSaveMission = theZone:getFlagValue(persistence.saveMission) end persistence.verbose = theZone.verbose persistence.saveNotification = theZone:getBoolFromZoneProperty("saveNotification", true) if persistence.verbose then trigger.action.outText("+++persistence: read config", 30) end end function persistence.start() -- lib check if not dcsCommon.libCheck then trigger.action.outText("persistence requires dcsCommon", 30) return false end if not dcsCommon.libCheck("persistence", persistence.requiredLibs) then return false end -- read config persistence.saveFileName = dcsCommon.getMissionName() .. " Data.txt" persistence.readConfigZone() -- let's see it lfs and io are online persistence.active = false if (not _G["lfs"]) or (not lfs) then if persistence.verbose then trigger.action.outText("+++persistence requires 'lfs'", 30) end return false end if not _G["io"] then if persistence.verbose then trigger.action.outText("+++persistence requires 'io'", 30) end return false end local mainDir = persistence.root .. persistence.serverDir -- usually DCS/Missions if not dcsCommon.stringEndsWith(mainDir, "\\") then mainDir = mainDir .. "\\" end local sharedDir = mainDir .. persistence.sharedDir -- ends on \\, hardwired persistence.sharedDir = sharedDir -- lets see if we can access the server's mission directory and -- save directory if persistence.isDir(mainDir) then if persistence.verbose then trigger.action.outText("persistence: main dir is <" .. mainDir .. ">", 30) end else if persistence.verbose then trigger.action.outText("+++persistence: Main directory <" .. mainDir .. "> not found or not a directory", 30) end return false end persistence.mainDir = mainDir local missionDir = mainDir .. persistence.saveDir if not dcsCommon.stringEndsWith(missionDir, "\\") then missionDir = missionDir .. "\\" end -- check if mission dir exists already local success, mode = persistence.hasFile(missionDir) if success and mode == "directory" then -- has been allocated, and is dir if persistence.verbose then trigger.action.outText("+++persistence: saving mission data to <" .. missionDir .. ">", 30) end elseif success then if persistence.verbose then trigger.action.outText("+++persistence: <" .. missionDir .. "> is not a directory", 30) end return false else -- does not exist, try to allocate it if persistence.verbose then trigger.action.outText("+++persistence: will now create <" .. missionDir .. ">", 30) end local ok, mkErr = lfs.mkdir(missionDir) if not ok then if persistence.verbose then trigger.action.outText("+++persistence: unable to create <" .. missionDir .. ">: <" .. mkErr .. ">", 30) end return false end if persistence.verbose then trigger.action.outText("+++persistence: created <" .. missionDir .. "> successfully, will save mission data here", 30) end end -- make sure that SHARED dir exists, create if not local success, mode = persistence.hasFile(sharedDir) if success and mode == "directory" then -- has been allocated, and is dir if persistence.verbose then trigger.action.outText("+++persistence: saving SHARED data to <" .. sharedDir .. ">", 30) end elseif success then if persistence.verbose then trigger.action.outText("+++persistence: <" .. sharedDir .. "> is not a directory", 30) end return false else -- does not exist, try to allocate it if persistence.verbose then trigger.action.outText("+++persistence: will now create <" .. sharedDir .. ">", 30) end local ok, mkErr = lfs.mkdir(sharedDir) if not ok then if persistence.verbose then trigger.action.outText("+++persistence: unable to create <" .. sharedDir .. ">: <" .. mkErr .. ">", 30) end return false end if persistence.verbose then trigger.action.outText("+++persistence: created <" .. sharedDir .. "> successfully, will save SHARED data here", 30) end end -- missionDir is root + serverDir + saveDir persistence.missionDir = missionDir persistence.active = true -- we can load and save data trigger.action.setUserFlag("cfxPersistence", 1) persistence.hasData = false -- we do not have save data -- from here on we can read and write files in the missionDir -- read persistence attributes from all zones local attrZones = cfxZones.getZonesWithAttributeNamed("saveFlags") for k, aZone in pairs(attrZones) do persistence.collectFlagsFromZone(aZone) -- process attributes -- we do not retain the zone, it's job is done end if persistence.verbose then trigger.action.outText("+++persistence is active", 30) end -- we now see if we can and need load data persistence.missionStartDataLoad() -- and start updating persistence.update() return persistence.active end -- -- go! -- if not persistence.start() then if persistence.verbose then trigger.action.outText("+++ persistence not available", 30) end -- we do NOT remove the methods so we don't crash end