local self = {} local binser = require('stdlib.binser') require('stdlib.util') local scheduler = require('stdlib.scheduler') local savedataName local savedata = { _lastSave = {0, 0, 0, 0, 0} } local loadedSavedata = false self.saveNextFrame = false ---@param name string @a unique name for the entire modfile ---@param forceIgnore boolean | nil --- for picking a name: --- please head to this link: --- https://www.random.org/strings/?num=1&len=16&digits=on&upperalpha=on&loweralpha=on&unique=on&format=plain&rnd=new --- and generate yourself a hash. afterwards, append it to your game name like so: --- savedata.initializeModule('myGameName_doAmaUOBIjiaSWyz') --- this helps ensure no collisions happen, breaking both of the modfiles in the process! --- **DON'T CHANGE THIS!!** else you'll break existing saves function self.initializeModule(name, forceIgnore) if loadedSavedata then error('uranium: cannot initialize savedata module after uranium.init!', 3) end if not forceIgnore then if #name < 16 then error('uranium: savedata name too short! must be at least 16 characters long', 2) end local normalChars = 0 local specialChars = 0 for i = 1, #name do local char = string.sub(name, i, i) local special = string.lower(char) == string.upper(char) if special then specialChars = specialChars + 1 else normalChars = normalChars + 1 end end if normalChars < 4 then error('uranium: savedata name should have at least 4 non-special characters!', 2) end if specialChars < 2 then error('uranium: savedata name should have at least 2 special characters!', 2) end end savedataName = name end local function checkIfInitialized() if not savedataName then error('uranium: savedata not initialized! please run savedata.initializeModule first', 3) end end local function convertSource(s) local pathSplit = split(s.source, '/') local lastTwo = pathSplit[#pathSplit - 1] .. '/' .. string.sub(pathSplit[#pathSplit], 1, -5) return lastTwo end ---@param data table ---@param name string | nil function self.s(data, name) if not name then name = convertSource(debug.getinfo(2, 'S')) end checkIfInitialized() if loadedSavedata then error('uranium: cannot add new savedata after initializing!', 3) end if string.sub(name, 1, 1) == '_' then error('uranium: cannot start savedata name with an underscore', 2) end savedata[name] = data end --- prefers tab1 with type mismatches; prefers tab2 with value mismatches local function mergeTable(tab1, tab2) local tab = {} for k, v1 in pairs(tab1) do local v2 = tab2[k] if type(v1) ~= type(v2) then tab[k] = v1 else if type(v1) == 'table' then tab[k] = mergeTable(v1, v2) else tab[k] = v2 end end end return tab end --- replaces tab1 with tab2, preserving pointers --- this sucks local function replaceFromRoot(tab1, tab2) local keys = {} for k in pairs(tab1) do if not includes(keys, k) then table.insert(keys, k) end end for k in pairs(tab2) do if not includes(keys, k) then table.insert(keys, k) end end for _, key in ipairs(keys) do if type(tab1[key]) == type(tab2[key]) and type(tab1[key]) == 'table' then replaceFromRoot(tab1[key], tab2[key]) else tab1[key] = tab2[key] end end end function self.load() checkIfInitialized() print('loading...') local profile = PROFILEMAN:GetMachineProfile():GetSaved() local save = {} local serialized = profile[savedataName] if not serialized then print('no savedata found') save = savedata else print('savedata found: ' .. serialized) save = binser.deserializeN(serialized, 1) print('deserialized: ' .. fullDump(save)) end local newSavedata = mergeTable(savedata, save) replaceFromRoot(savedata, newSavedata) print('merged: ' .. fullDump(savedata)) end ---@param instant boolean | nil function self.save(instant) checkIfInitialized() print('saving...') local profile = PROFILEMAN:GetMachineProfile():GetSaved() savedata._lastSave = {Hour(), Minute(), DayOfMonth(), MonthOfYear(), Year()} print('savedata: ' .. fullDump(savedata)) local serialized = binser.serialize(savedata) print('serialized: ' .. serialized) profile[savedataName] = serialized uranium:call('save') if instant then PROFILEMAN:SaveMachineProfile() uranium:call('saveFinished') else self.saveNextFrame = true scheduler:scheduleInTicks(2, function() self.saveNextFrame = false PROFILEMAN:SaveMachineProfile() uranium:call('saveFinished') end) end end function self.getLastSave() checkIfInitialized() if savedata._lastSave and savedata._lastSave[1] == 0 and savedata._lastSave[2] == 0 and savedata._lastSave[3] == 0 and savedata._lastSave[4] == 0 and savedata._lastSave[5] == 0 then return nil else return savedata._lastSave end end function uranium.init() loadedSavedata = true if savedataName then self.load() end end function self.enableAutosave() checkIfInitialized() function uranium.exit() self.save(true) end end return self