Savefile.lua 23.66 KiB
-- TE4 - T-Engine 4
-- Copyright (C) 2009 - 2017 Nicolas Casalini
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
--
-- Nicolas Casalini "DarkGod"
-- darkgod@te4.org
require "engine.class"
local Dialog = require "engine.ui.Dialog"
--[[-- Savefile code
T-Engine4 savefiles are direct serialization of in game objects
Basically the engine is told to save your Game instance and then it will recursively save all that it contains: level, map, entities, your own objects, ...
The savefile structure is a zip file that contains one file per object to be saved. Unzip one, it is quite obvious
A simple object (that does not do anything too fancy in its constructor) will save/load without anything to code, it's magic!
For more complex objects, look at the methods save() and loaded() in objects that have them
]]
--- @classmod engine.Savefile
module(..., package.seeall, class.make)
_M.current_save = false
_M.hotkeys_file = "/save/quick_hotkeys"
_M.md5_types = {}
--- Init a savefile
-- @param savefile the name of the savefile, usually the player's name. It will be sanitized so dont bother doing it
-- @param coroutine if true the saving will yield sometimes to let other code run
function _M:init(savefile, coroutine)
self.short_name = savefile:gsub("[^a-zA-Z0-9_-.]", "_"):lower()
self.save_dir = "/save/"..self.short_name.."/"
self.quickbirth_file = "/save/"..self.short_name..".quickbirth"
self.load_dir = "/tmp/loadsave/"
print("Loading savefile ", self.save_dir)
self.coroutine = coroutine
self.tables = {}
self.process = {}
self.loaded = {}
self.delayLoad = {}
_M.current_save = self
end
--- The save file that is currently open
-- @static
-- @return current save
function _M:getCurrent()
return _M.current_save
end
--- Set this save file as the current one
-- @static
-- @param[type=table] save
-- @return current save
function _M:setCurrent(save)
_M.current_save = save
end
--- Do we md5 the specified type when saving?
-- @static
-- @string type
function _M:setSaveMD5Type(type)
self.md5_types[type] = true
end
--- Finishes up a savefile
-- Always call it once done
function _M:close()
self.tables = nil
self.process = nil
self.loaded = nil
self.delayLoad = nil
self.current_save_main = nil
end
--- Delete the savefile, if needed
-- Also deletes from steam cloud if it's available
function _M:delete()
for i, f in ipairs(fs.list(self.save_dir)) do
fs.delete(self.save_dir..f)
if util.steamCanCloud() then core.steam.deleteFile(self.save_dir..f) end
end
fs.delete(self.save_dir)
if util.steamCanCloud() then
local namespace = core.steam.getFileNamespace()
local list = core.steam.listFilesStartingWith(namespace..self.save_dir)
core.steam.setFileNamespace("")
for i, file in ipairs(list) do
core.steam.deleteFile(file)
end
core.steam.setFileNamespace(namespace)
end
end
--- add to process
function _M:addToProcess(o)
if not self.tables[o] then
table.insert(self.process, o)
self.tables[o] = true
end
end
--- Add a delayed load for object
-- @param o
function _M:addDelayLoad(o)
-- print("add delayed", _M, "::", self, #self.delayLoad, o)
table.insert(self.delayLoad, 1, o)
end
--- Get the filename of the object
-- @param o
-- @return[1] "main"
-- @return[2] o.__CLASSNAME.."-"..tostring(o):sub(8)
function _M:getFileName(o)
if o == self.current_save_main then
return "main"
else
return o.__CLASSNAME.."-"..tostring(o):sub(8)
end
end
--- Save the object to specified zip
-- @param obj current_save_main
-- @param zip
-- @return int of how many processed
function _M:saveObject(obj, zip)
local processed = 0
self.current_save_zip = zip
self.current_save_main = obj
self:addToProcess(obj)
while #self.process > 0 do
local tbl = table.remove(self.process)
self.tables[tbl] = self:getFileName(tbl)
if tbl.onSaving then tbl:onSaving() end
tbl:save()
savefile_pipe.current_nb = savefile_pipe.current_nb + 1
processed = processed + 1
core.wait.manualTick(1)
if self.coroutine then coroutine.yield() end
end
return processed
end
--- Get a savename for a world
-- @param[type=World] world unused
-- @return "world.teaw"
function _M:nameSaveWorld(world)
return "world.teaw"
end
--- Get a savename for a world
-- @return "world.teaw"
function _M:nameLoadWorld()
return "world.teaw"
end
--- Save the given world
-- @param[type=World] world the world to save
-- @param[type=boolean] no_dialog show a popup when saving?
function _M:saveWorld(world, no_dialog)
collectgarbage("collect")
fs.mkdir(self.save_dir)
local popup
if not no_dialog then
popup = Dialog:simpleWaiter("Saving world", "Please wait while saving the world...")
end
core.display.forceRedraw()
local zip = (self.save_dir..self:nameSaveWorld(world)..".tmp")
local nb = self:saveObject(world, zip)
--zip:add("nb", tostring(nb))
--fs.delete(self.save_dir..self:nameSaveWorld(world))
--fs.rename(self.save_dir..self:nameSaveWorld(world)..".tmp", self.save_dir..self:nameSaveWorld(world))
savefile_pipe:pushGeneric("saveWorld_md5", function() self:md5Upload("world", self:nameSaveWorld(world)) end)
if not no_dialog then popup:done() end
end
--- Save the given birth descriptors, used for quick start
-- @param[type=table] descriptor {key, value}
function _M:saveQuickBirth(descriptor)
collectgarbage("collect")
local f = fs.open(self.quickbirth_file, "w")
for k, e in pairs(descriptor) do
f:write(("%s = %q\n"):format(tostring(k), tostring(e)))
end
f:close()
end
--- Load the given birth descriptors, used for quick start
-- @return[1] nil
-- @return[2] table
function _M:loadQuickBirth()
collectgarbage("collect")
local f = loadfile(self.quickbirth_file)
print("[QUICK BIRTH]", f)
if f then
-- Call the file body inside its own private environment
local def = {}
setfenv(f, def)
if pcall(f) then
return def
end
end
return nil
end
--- Saves the screenshot of a game
-- @param screenshot the screenshot to write to
function _M:saveScreenshot(screenshot)
if not screenshot then return end
fs.mkdir(self.save_dir)
local f = fs.open(self.save_dir.."cur.png", "w")
f:write(screenshot)
f:close()
end
--- Get a savename for a game
-- @param[type=Game] game
-- @return "game.teag"
function _M:nameSaveGame(game)
return "game.teag"
end
--- Get a savename for a game
-- @return "game.teag"
function _M:nameLoadGame()
return "game.teag"
end
--- Save the given game
-- @param[type=Game] game
-- @param[type=boolean] no_dialog Show a popup when saving?
function _M:saveGame(game, no_dialog)
collectgarbage("collect")
fs.mkdir(self.save_dir)
local popup
if not no_dialog then
popup = Dialog:simpleWaiter("Saving game", "Please wait while saving the game...")
end
core.display.forceRedraw()
local zip = (self.save_dir..self:nameSaveGame(game)..".tmp")
local nb = self:saveObject(game, zip)
--zip:add("nb", tostring(nb))
--fs.delete(self.save_dir..self:nameSaveGame(game))
--fs.rename(self.save_dir..self:nameSaveGame(game)..".tmp", self.save_dir..self:nameSaveGame(game))
savefile_pipe:pushGeneric("saveGame_md5", function() self:md5Upload("game", self:nameSaveGame(game)) end)
local desc = game:getSaveDescription()
local f = fs.open(self.save_dir.."desc.lua", "w")
f:write(("module = %q\n"):format(game.__mod_info.short_name))
f:write(("module_version = {%d,%d,%d}\n"):format(game.__mod_info.version[1], game.__mod_info.version[2], game.__mod_info.version[3]))
local addons = {}
for add, _ in pairs(game.__mod_info.addons) do addons[#addons+1] = "'"..add.."'" end
f:write(("addons = {%s}\n"):format(table.concat(addons, ", ")))
f:write(("name = %q\n"):format(desc.name))
f:write(("short_name = %q\n"):format(self.short_name))
f:write(("timestamp = %d\n"):format(os.time()))
f:write(("loadable = %s\n"):format(game:isLoadable() and "true" or "false"))
f:write(("cheat = %s\n"):format(game:isTainted() and "true" or "false"))
f:write(("description = %q\n"):format(desc.description))
f:close()
if util.steamCanCloud() then core.steam.writeFile(self.save_dir.."desc.lua") end
-- TODO: Replace this with saving quickhotkeys to the profile.
-- Add print_doable_table to utils.lua as table.print_doable?
local f = fs.open(_M.hotkeys_file, "w")
local function print_doable_table(src, str, print_func, only, skip, is_first_call)
str = str or ""
print_func = print_func or print
if is_first_call then print_func(str .. " = {}") end
for k, e in pairs(src) do
local new_str = str
if (not skip or (skip and not skip[k])) and (not only or (only and only == k)) then
if type(e) == "table" then
if type(k) ~= "string" then new_str = str .. ("[%s]"):format(tostring(k)) else new_str = str .. ("[%q]"):format(tostring(k)) end
print_func(new_str .. " = {}")
print_doable_table(e, new_str, print_func)
else
if type(k) ~= "string" then new_str = new_str .. ("[%s]"):format(tostring(k)) else new_str = new_str .. ("[%q]"):format(tostring(k)) end
if type(e) ~= "string" then print_func(new_str .. (" = %s"):format(tostring(e))) else print_func(new_str .. (" = %q"):format(tostring(e))) end
end
end
end
end
print_doable_table(engine.interface.PlayerHotkeys.quickhotkeys, "quickhotkeys", function(s) return f:write(s .. "\n") end, "Player: Global", nil, true)
print_doable_table(engine.interface.PlayerHotkeys.quickhotkeys, "quickhotkeys", function(s) return f:write(s .. "\n") end, "Player: Specific", nil, false)
print_doable_table(engine.interface.PlayerHotkeys.quickhotkeys, "quickhotkeys", function(s) return f:write(s .. "\n") end, nil, {["Player: Global"] = true, ["Player: Specific"] = true}, false)
f:close()
if not no_dialog then popup:done() end
end
--- Get a savename for a zone
-- @param[type=Zone] zone
-- @return "zone-%s.teaz"
function _M:nameSaveZone(zone)
return ("zone-%s.teaz"):format(zone.short_name)
end
--- Get a savename for a zone
-- @param[type=Zone] zone
-- @return "zone-%s.teaz"
function _M:nameLoadZone(zone)
return ("zone-%s.teaz"):format(zone)
end
--- Save a zone
-- @param[type=Zone] zone
-- @param[type=?boolean] no_dialog Show a popup when saving?
function _M:saveZone(zone, no_dialog)
fs.mkdir(self.save_dir)
local popup
if not no_dialog then
popup = Dialog:simpleWaiter("Saving zone", "Please wait while saving the zone...")
end
core.display.forceRedraw()
local zip = (self.save_dir..self:nameSaveZone(zone)..".tmp")
local nb = self:saveObject(zone, zip)
--zip:add("nb", tostring(nb))
--fs.delete(self.save_dir..self:nameSaveZone(zone))
--fs.rename(self.save_dir..self:nameSaveZone(zone)..".tmp", self.save_dir..self:nameSaveZone(zone))
savefile_pipe:pushGeneric("saveZone_md5", function() self:md5Upload("zone", self:nameSaveZone(zone)) end)
if not no_dialog then popup:done() end
end
--- Get a savename for a level
-- @param[type=Level] level
-- @return "level-%s-%d.teal"
function _M:nameSaveLevel(level)
return ("level-%s-%d.teal"):format(level.data.short_name, level.level)
end
--- Get a savename for a level
-- @param[type=Zone] zone
-- @param[type=Level] level
-- @return "level-%s-%d.teal"
function _M:nameLoadLevel(zone, level)
return ("level-%s-%d.teal"):format(zone, level)
end
--- Save a level
-- @param[type=Level] level
-- @param[type=?boolean] no_dialog Show a popup when saving?
function _M:saveLevel(level, no_dialog)
fs.mkdir(self.save_dir)
local popup
if not no_dialog then
popup = Dialog:simpleWaiter("Saving level", "Please wait while saving the level...")
end
core.display.forceRedraw()
local zip = (self.save_dir..self:nameSaveLevel(level)..".tmp")
local nb = self:saveObject(level, zip)
--zip:add("nb", tostring(nb))
--fs.delete(self.save_dir..self:nameSaveLevel(level))
--fs.rename(self.save_dir..self:nameSaveLevel(level)..".tmp", self.save_dir..self:nameSaveLevel(level))
savefile_pipe:pushGeneric("saveLevel_md5", function() self:md5Upload("level", self:nameSaveLevel(level)) end)
if not no_dialog then popup:done() end
end
--- Get a savename for an entity
-- @return "entity-%s.teae"
function _M:nameSaveEntity(e)
return ("entity-%s.teae"):format(e.name:gsub("[^a-zA-Z0-9_-.]", "_"):lower())
end
--- Get a savename for an entity
-- @return "entity-%s.teae"
function _M:nameLoadEntity(name)
return ("entity-%s.teae"):format(name:gsub("[^a-zA-Z0-9_-.]", "_"):lower())
end
--- Save an entity
-- @param[type=Entity] e
-- @param[type=?boolean] no_dialog Show a popup when saving?
function _M:saveEntity(e, no_dialog)
fs.mkdir(self.save_dir)
local popup
if not no_dialog then
popup = Dialog:simpleWaiter("Saving entity", "Please wait while saving the entity...")
end
core.display.forceRedraw()
local zip = (self.save_dir..self:nameSaveEntity(e)..".tmp")
local nb = self:saveObject(e, zip)
--zip:add("nb", tostring(nb))
--fs.delete(self.save_dir..self:nameSaveEntity(e))
--fs.rename(self.save_dir..self:nameSaveEntity(e)..".tmp", self.save_dir..self:nameSaveEntity(e))
savefile_pipe:pushGeneric("saveEntity_md5", function() self:md5Upload("entity", self:nameSaveEntity(e)) end)
if not no_dialog then popup:done() end
end
--- Ensure compatability between saves
-- @param o object
-- @param base base object
-- @param allow_object allow the object to save (called recursively)
local function resolveSelf(o, base, allow_object)
-- we check both to ensure compatibility with old saves; including world.teaw which is vital to not make everything explode
if (o.__ATOMIC or o.__CLASSNAME) and not allow_object then return end
local change = {}
for k, e in pairs(o) do
if type(e) == "table" then
if e == class.LOAD_SELF then change[#change+1] = k
else resolveSelf(e, base, false)
end
end
end
for i, k in ipairs(change) do o[k] = base end
end
--- Actually load an object
-- @param load
-- @return[1] nil
-- @return[2] o
function _M:loadReal(load)
if self.loaded[load] then return self.loaded[load] end
local f = fs.open(self.load_dir..load, "r")
if not f then return nil end
local lines = {}
while true do
local l = f:read()
if not l then break end
lines[#lines+1] = l
end
f:close()
local o = class.load(table.concat(lines), load)
-- Resolve self referencing tables now
resolveSelf(o, o, true)
core.wait.manualTick(1)
self.loaded[load] = o
return o
end
--- Loads a `World`
-- @return[1] nil
-- @return[1] "no savefile"
-- @return[2] `World`
function _M:loadWorld()
if util.steamCanCloud() then core.steam.readFile(self.save_dir..self:nameLoadWorld()) end
local path = fs.getRealPath(self.save_dir..self:nameLoadWorld())
if not path or path == "" then return nil, "no savefile" end
local checker = self:md5Check("world", self:nameLoadWorld())
fs.mount(path, self.load_dir)
local popup = Dialog:simpleWaiter("Loading world", "Please wait while loading the world...")
core.display.forceRedraw()
local loadedWorld = self:loadReal("main")
-- Delay loaded must run
for i, o in ipairs(self.delayLoad) do
-- print("loader executed for class", o, o.__CLASSNAME)
o:loaded()
end
fs.umount(path)
-- We check for the server return, while we loaded the save it probably responded so we do not wait at all
if not checker() then self:badMD5Load() end
popup:done()
return loadedWorld
end
--- Gets the filesize of a `World` savefile
-- @return[1] nil
-- @return[1] "no savefile"
-- @return[2] size
function _M:loadWorldSize()
if util.steamCanCloud() then core.steam.readFile(self.save_dir..self:nameLoadWorld()) end
local path = fs.getRealPath(self.save_dir..self:nameLoadWorld())
if not path or path == "" then return nil, "no savefile" end
fs.mount(path, self.load_dir)
local f = fs.open(self.load_dir.."nb", "r")
local nb = 0
if f then
nb = tonumber(f:read()) or 100
f:close()
end
fs.umount(path)
return nb
end
--- Loads a `Game`
-- delay_fct is all of the delayLoad functionality for the save file
-- @return[1] nil
-- @return[1] "no savefile"
-- @return[2] `Game`
-- @return[2] delay_fct
function _M:loadGame()
if util.steamCanCloud() then core.steam.readFile(self.save_dir..self:nameLoadGame()) end
local path = fs.getRealPath(self.save_dir..self:nameLoadGame())
if not path or path == "" then return nil, "no savefile" end
fs.mount(path, self.load_dir)
local popup = Dialog:simpleWaiter("Loading game", "Please wait while loading the game...")
core.display.forceRedraw()
local loadedGame = self:loadReal("main")
local checker = self:md5Check("game", self:nameLoadGame(), loadedGame)
-- Delay loaded must run
local delay_fct = function()
for i, o in ipairs(self.delayLoad) do
-- print("loader executed for class", o, o.__CLASSNAME)
o:loaded()
end
end
fs.umount(path)
-- We check for the server return, while we loaded the save it probably responded so we do not wait at all
if not checker() then self:badMD5Load() end
popup:done()
return loadedGame, delay_fct
end
--- Gets the filesize of the `Game` savefile
-- @return[1] nil
-- @return[1] "no savefile"
-- @return[2] size
function _M:loadGameSize()
if util.steamCanCloud() then core.steam.readFile(self.save_dir..self:nameLoadGame()) end
local path = fs.getRealPath(self.save_dir..self:nameLoadGame())
if not path or path == "" then return nil, "no savefile" end
fs.mount(path, self.load_dir)
local f = fs.open(self.load_dir.."nb", "r")
local nb = 0
if f then
nb = tonumber(f:read()) or 100
f:close()
end
fs.umount(path)
return nb
end
--- Loads a `Zone`
-- executes all delayLoad automatically
-- @param[type=table] zone
-- @return[1] false
-- @return[2] `Zone`
function _M:loadZone(zone)
if util.steamCanCloud() then core.steam.readFile(self.save_dir..self:nameLoadZone(zone)) end
local path = fs.getRealPath(self.save_dir..self:nameLoadZone(zone))
if not path or path == "" then return false end
local checker = self:md5Check("zone", self:nameLoadZone(zone))
fs.mount(path, self.load_dir)
local f = fs.open(self.load_dir.."nb", "r")
local nb = 0
if f then
nb = tonumber(f:read()) or 100
f:close()
end
local popup = Dialog:simpleWaiter("Loading zone", "Please wait while loading the zone...", nil, nil, nb > 0 and nb)
core.wait.enableManualTick(true)
core.display.forceRedraw()
local loadedZone = self:loadReal("main")
-- Delay loaded must run
for i, o in ipairs(self.delayLoad) do
-- print("loader executed for class", o, o.__CLASSNAME)
o:loaded()
end
-- We check for the server return, while we loaded the save it probably responded so we do not wait at all
if not checker() then self:badMD5Load() end
core.wait.enableManualTick(false)
popup:done()
fs.umount(path)
return loadedZone
end
--- Loads a `Level`
-- all delayLoad executes automatically
-- @param[type=table] zone
-- @param[type=table] level
-- @return[1] false
-- @return[2] `Level`
function _M:loadLevel(zone, level)
if util.steamCanCloud() then core.steam.readFile(self.save_dir..self:nameLoadLevel(zone, level)) end
local path = fs.getRealPath(self.save_dir..self:nameLoadLevel(zone, level))
if not path or path == "" then return false end
local checker = self:md5Check("level", self:nameLoadLevel(zone, level))
fs.mount(path, self.load_dir)
local f = fs.open(self.load_dir.."nb", "r")
local nb = 0
if f then
nb = tonumber(f:read()) or 100
f:close()
end
local popup = Dialog:simpleWaiter("Loading level", "Please wait while loading the level...", nil, nil, nb > 0 and nb)
core.display.forceRedraw()
local loadedLevel = self:loadReal("main")
-- Delay loaded must run
for i, o in ipairs(self.delayLoad) do
-- print("loader executed for class", o, o.__CLASSNAME)
o:loaded()
end
-- We check for the server return, while we loaded the save it probably responded so we do not wait at all
if not checker() then self:badMD5Load() end
popup:done()
fs.umount(path)
return loadedLevel
end
--- Loads an entity
-- automatically executes delayLoad
-- @string name
-- @return[1] false
-- @return[2] `Entity`
function _M:loadEntity(name)
if util.steamCanCloud() then core.steam.readFile(self.save_dir..self:nameLoadEntity(name)) end
local path = fs.getRealPath(self.save_dir..self:nameLoadEntity(name))
if not path or path == "" then return false end
local checker = self:md5Check("entity", self:nameLoadEntity(name))
fs.mount(path, self.load_dir)
local f = fs.open(self.load_dir.."nb", "r")
local nb = 0
if f then
nb = tonumber(f:read()) or 100
f:close()
end
local popup = Dialog:simpleWaiter("Loading entity", "Please wait while loading the entity...", nil, nil, nb > 0 and nb)
core.display.forceRedraw()
local loadedEntity = self:loadReal("main")
-- Delay loaded must run
local ok = false
pcall(function()
for i, o in ipairs(self.delayLoad) do
-- print("loader executed for class", o, o.__CLASSNAME)
o:loaded()
end
ok = true
end)
-- We check for the server return, while we loaded the save it probably responded so we do not wait at all
if ok and not checker() then self:badMD5Load() end
popup:done()
fs.umount(path)
if not ok then return false end
return loadedEntity
end
--- Checks validity of a kind
-- @string type "Entity" | "World" | "Level" | "Zone"
-- @param object the object to check
-- @return true if valid
function _M:checkValidity(type, object)
local path = fs.getRealPath(self.save_dir..self['nameSave'..type:lower():capitalize()](self, object))
if not path or path == "" then
print("[SAVEFILE] checked validity of type", type, " => path not found")
print("[SAVEFILE] path info", self.save_dir..self['nameSave'..type:lower():capitalize()](self, object), "=>", path)
return false
end
fs.mount(path, self.load_dir)
local ok = false
local f = fs.open(self.load_dir.."main", "r")
if f then ok = true f:close() end
fs.umount(path)
print("[SAVEFILE] checked validity of type", type, " => ", ok and "all fine" or "main not found")
return ok
end
--- Checks for existence
-- @return true if exists
function _M:check()
if util.steamCanCloud() then core.steam.readFile(self.save_dir..self:nameLoadGame()) end
return fs.exists(self.save_dir..self:nameLoadGame())
end
--- Upload type as md5
-- @see setSaveMD5Type
-- @string type savefile type
-- @string f filename
function _M:md5Upload(type, f)
if not self.md5_types[type] then return end
local p = game:getPlayer(true)
if not p.getUUID then return end
local uuid = p:getUUID()
if not uuid then return end
local md5 = require "md5"
local m = nil
local fff = fs.open(self.save_dir..f, "r")
if fff then
local data = fff:read(10485760)
if data and data ~= "" then
m = md5.sumhexa(data)
end
fff:close()
end
print("Save MD5", uuid, m, type, self.save_dir..f)
profile:setSaveID(game.__mod_info.short_name, uuid, f, m)
end
--- Check if md5 is correct
-- @see setSaveMD5Type
-- @see PlayerProfile:checkSaveID
-- @string type savefile type
-- @string f filename
-- @param[opt=`Game`] loadgame game we're loading, defaults to static Game
-- @return[1] fct() return false end
-- @return[2] fct() return true end
function _M:md5Check(type, f, loadgame)
if not self.md5_types[type] then return function() return true end end
local bad = function() return false end
local p = (loadgame or game):getPlayer(true)
if not p.getUUID then return bad end
local uuid = p:getUUID()
if not uuid then return bad end
local md5 = require "md5"
local m = nil
local fff = fs.open(self.save_dir..f, "r")
if fff then
local data = fff:read(10485760)
if data and data ~= "" then
m = md5.sumhexa(data)
end
fff:close()
end
print("Check MD5", uuid, m, type, f)
return profile:checkSaveID(game.__mod_info.short_name, uuid, f, m)
end
--- Called when our md5 is bad
function _M:badMD5Load()
if game.onBadMD5Load then game:onBadMD5Load() end
game.bad_md5_loaded = true
end