DML/modules/persistence.lua
Christian Franz 61f33561fc Version 2.0.2
New scribe, names, better csarManager
2024-02-08 13:04:35 +01:00

607 lines
17 KiB
Lua

persistence = {}
persistence.version = "2.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
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)
if not persistence.active then return nil end
if not persistence.hasData then return nil end
if not persistence.missionData then return 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, "<err>"
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 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
return false
end
theFile:write(theString)
theFile:close()
return true
end
function persistence.loadText(fileName) -- load file as text
if not persistence.active then return nil end
if not fileName then return nil end
local path = persistence.missionDir .. fileName
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) -- load file as table
if not persistence.active then return nil end
if not fileName then return nil end
local t = persistence.loadText(fileName)
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.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
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
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 = "<NIL>" end
if not vid then vid = "<NIL>" 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.saveMissionData()
local myData = {}
-- 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 = callbacks.persistData()
if moduleData then
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)
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\\")
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?", "*<none>")
persistence.lastCleanRestart = theZone:getFlagValue(persistence.cleanRestart)
end
if theZone:hasProperty("saveMission?") then
persistence.saveMission = theZone:getStringFromZoneProperty("saveMission?", "*<none>")
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
if not dcsCommon.stringEndsWith(mainDir, "\\") then
mainDir = mainDir .. "\\"
end
-- 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
-- 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