Newer
Older
-- TE4 - T-Engine 4
--
-- 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"
--- Savefile code
-- Creates a savefile pipe, savefiles requests can be pushed into it, it will execute them
-- in order and, as much as possible, as a background process, thus not preventing the game from running.<br/>
-- There can only be one pipe! DO NOT TRY TO MAKE MORE
-- @classmod engine.SavefilePipe
module(..., package.seeall, class.make)
_M.current = nil
--- Init a savefile pipe
-- @param class the name of the Savefile class to use. Defaults to engine.Savefile
-- @param max_before_wait Number of saves allowed in the pipe before the game pauses to do the saves (this is to prevent pushing too many saves and making it very very long)
function _M:init(class, max_before_wait)
assert(not _M.current, "Tried to create more than one savefile pipe!")
_M.current = self
self.saveclass = class or "engine.Savefile"
self.pipe = {}
self.on_done = {}
self.co = nil
--- Cleanup old saves in steam cloud if needed
function _M:steamCleanup()
if not util.steamCanCloud() then return end
local oldns = core.steam.getFileNamespace()
core.steam.setFileNamespace("")
print("[SAVEFILE PIPE] Cleaning up steam cloud")
local avail = core.steam.getCloudSize() / 1024 / 1024
print("[SAVEFILE PIPE] Cloud has "..avail.."MB available")
-- Still have room left
if avail >= MAX_ALLOWED then core.steam.setFileNamespace(oldns) return end
print("[SAVEFILE PIPE] Steam cloud missing space, cleaning up old saves")
local saves = {}
local list = core.steam.listFilesEndingWith("game.teag")
for _, file in ipairs(list) do
local _, _, modname, char = file:find("^([^/]+)/save/([^/]+)/game%.teag$")
if modname then
local ts = core.steam.timestampFile(file)
saves[#saves+1] = {file=file, ts=ts, name=char, mod=modname}
end
end
table.sort(saves, function(a, b) return a.ts < b.ts end)
-- Forget until we have space
while avail < MAX_ALLOWED and #saves > 0 do
local save = table.remove(saves, 1)
print("[SAVEFILE PIPE] Cloud Forgetting save", save.mod, save.name)
for _, file in ipairs(core.steam.listFilesStartingWith(save.mod.."/save/"..save.name.."/")) do
print("\t* "..file)
core.steam.forgetFile(file)
end
avail = core.steam.getCloudSize() / 1024 / 1024
print("\t => Cloud has "..avail.."MB available")
end
core.steam.setFileNamespace(oldns)
end
--- Disables/enables clean saves
-- @param v true or false
function _M:ignoreCloudSave(v)
self.disable_cloud_saves = v
end
--- Push a savefile request
-- @param savename the name of the savefile to handle
-- @param type the Savefile method to use. I.e: "game", "level", "zone". This will cann the Savefile:saveGame, Savefile:saveLevel, Savefile:saveZone methods
-- @param object the object to save
-- @param class a class name, if different from the default one
-- @param on_end a function to call when this object is saved; this can be nil
function _M:push(savename, type, object, class, on_end)
if game.onSavefilePush then game:onSavefilePush(savename, type, object, class) end
-- Cant save twice the same thing before it finishes
if self.pipe_types[type] and self.pipe_types[type][savename] then
print("[SAVEFILE PIPE] Already saving data", type, savename, "waiting for finish before piping...")
self:forceWait()
print("[SAVEFILE PIPE] All pipe saving emptied, resuming next save")
end
dg
committed
local screenshot = nil
if type == "game" then
screenshot = game:takeScreenshot(true)
end
class = class or self.saveclass
local Savefile = require(class)
local id = Savefile["nameSave"..type:lower():capitalize()](Savefile, object)
if #self.pipe == 0 then savefile_pipe.start_time = os.time() savefile_pipe.current_nb = 0 self:steamCleanup() end
self.pipe_types[type] = self.pipe_types[type] or {}
self.pipe_types[type][savename] = true
self.pipe[#self.pipe+1] = {id=id, savename = savename, type=type, object=clone, nb_objects=nb, baseobject=object, class=class, saveversion=game:saveVersion("new"), screenshot=screenshot, on_end=on_end}
local total_nb = 0
for i, p in ipairs(self.pipe) do total_nb = total_nb + p.nb_objects end
self.total_nb = total_nb
if not self.co or coroutine.status(self.co) == "dead" then
self.co = coroutine.create(function() return self:doThread() end)
game:registerCoroutine("savefilepipe", self.co)
end
if game.onSavefilePushed then game:onSavefilePushed(savename, type, object, class) end
-- Refuse to continue, make the user wait
if #self.pipe >= self.max_before_wait or not config.settings.background_saves then
self:forceWait()
end
end
--- Push a generic action to be done once saves complete
function _M:pushGeneric(name, fct)
self.on_done[#self.on_done+1] = {name=name, fct=fct}
end
--- Actually do the saves
-- This should run in a coroutine.<br/>
-- Do not call this, this is automatic!
function _M:doThread()
self.saving = true
collectgarbage("collect")
dg
committed
-- collectgarbage("stop")
if game:getPlayer() then game:getPlayer().changed = true end
while #self.pipe > 0 do
local p = self.pipe[1]
local Savefile = require(p.class)
local o = p.object
print("[SAVEFILE PIPE] new save running in the pipe:", p.savename, p.type, "::", p.id, "::", p.baseobject, "=>", p.object, "("..p.nb_objects..")")
o.__saved_saveversion = p.saveversion
save["save"..p.type:lower():capitalize()](save, o, true)
dg
committed
if p.screenshot then save:saveScreenshot(p.screenshot) end
p.save = save
waiton[save.current_save_zip:gsub("%.tmp$", "")] = p
save:close()
table.remove(self.pipe, 1)
end
-- Unleash the fury of the offthread saving
print("[SAVEFILE PIPE] unleashing save thread")
core.serial.threadSave()
-- Wait for feedback
while next(waiton) do
local pop = core.serial.popSaveReturn()
if not pop then coroutine.yield()
else
local dontremove = false
local p = waiton[pop]
if p then
local Savefile = require(p.class)
print("[SAVEFILE PIPE] Checking save", p.savename, p.type, p.save.current_save_zip)
local save = Savefile.new(p.savename, config.settings.background_saves)
save:close()
if not okmain then
print("[SAVEFILE PIPE] *RE*new save running in the pipe:", p.savename, p.type, "::", p.id, "::", p.baseobject, "=>", p.object, "("..p.nb_objects..")")
local o = p.object
local save = Savefile.new(p.savename, config.settings.background_saves)
o.__saved_saveversion = p.saveversion
save["save"..p.type:lower():capitalize()](save, o, true)
if p.screenshot then save:saveScreenshot(p.screenshot) end
p.save = save
waiton[save.current_save_zip:gsub("%.tmp$", "")] = p
save:close()
core.serial.threadSave()
print("[SAVEFILE PIPE] Resaving sent", p.savename, p.type, p.save.current_save_zip)
dontremove = true
end
end
if not dontremove then
if p and p.on_end then
p.on_end(p.save)
end
waiton[pop] = nil
if game.log then game.log("Saving done.") end
dg
committed
-- collectgarbage("restart")
self.saving = false
if game:getPlayer() then game:getPlayer().changed = true end
-- Run the generic stuff
while #self.on_done > 0 do
local p = self.on_done[1]
print("[SAVEFILE PIPE] on end", p.name)
p.fct()
table.remove(self.on_done, 1)
end
if savefile_pipe.start_time then
print("[SAVEFILE PIPE] saving took "..(os.time() - savefile_pipe.start_time).." seconds")
end
end
--- Force to wait for the saves
function _M:forceWait()
if #self.pipe == 0 and (not self.waiton or not next(self.waiton)) then return end
local popup = Dialog:simpleWaiter("Saving...", "Please wait while saving...", nil, 1000, self.total_nb)
core.display.forceRedraw()
while coroutine.status(self.co) ~= "dead" do
cnt = cnt + 1
if cnt == 1000 then core.display.forceRedraw() if game:getPlayer() then game:getPlayer().changed = true end cnt = 0 end
coroutine.resume(self.co)
end
end
--- Allow to ignore saveversion token
function _M:ignoreSaveToken(v)
self.ignore_save_token = v
end
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
--- Load a savefile
-- @param savename the name of the savefile to handle
-- @param type the Savefile method to use. I.e: "game", "level", "zone". This will cann the Savefile:saveGame, Savefile:saveLevel, Savefile:saveZone methods
-- @param class a class name, if different from the default one
function _M:doLoad(savename, type, class, ...)
class = class or self.saveclass
local Savefile = require(class)
local id = Savefile["nameLoad"..type:lower():capitalize()](Savefile, ...)
-- Look for it in the pipe
for i = 1, #self.pipe do
local p = self.pipe[i]
print("[SAVEFILE PIPE] looking for savefile to recover in the memory pipe", p.id, "?=?", id)
if p.id == id then
-- It is still in the save pipe: return it
print("[SAVEFILE PIPE] recovering save from memory in pipe:", savename, type, "::", id)
return p.baseobject
end
end
local cur = Savefile:getCurrent()
local save = Savefile.new(savename)
local ret = save["load"..type:lower():capitalize()](save, ...)
save:close()
Savefile:setCurrent(cur)
-- Check for validity
if _G.type(ret) == "table" and type ~= "game" and type ~= "world" and not self.ignore_save_token then
if not game:saveVersion(ret.__saved_saveversion) then
print("Loading savefile", savename, type, class," with id", ret.__saved_saveversion, "but current game does not know this token => ignoring")
return nil
end
end
return ret
end