diff --git a/game/engines/default/engine/Game.lua b/game/engines/default/engine/Game.lua index cc0172b23f5d3f820c316e2dd721cf1e0a64013a..b5a3ae1889870e9563ba23c106b2334b8c9a5488 100644 --- a/game/engines/default/engine/Game.lua +++ b/game/engines/default/engine/Game.lua @@ -100,7 +100,7 @@ function _M:defaultSavedFields(t) always_target=true, gfxmode=true, uniques=true, object_known_types=true, memory_levels=true, achievement_data=true, factions=true, playing_musics=true, state=true, - __savefile_version_tokens = true, + __savefile_version_tokens = true, bad_md5_loaded = true, } table.merge(def, t) return def diff --git a/game/engines/default/engine/PlayerProfile.lua b/game/engines/default/engine/PlayerProfile.lua index 555bf074ae647ceb47a4329820c97ced35fb9308..26b5be7fcae9f2989c0056d325bfa988c7c72658 100644 --- a/game/engines/default/engine/PlayerProfile.lua +++ b/game/engines/default/engine/PlayerProfile.lua @@ -663,6 +663,35 @@ function _M:registerSaveChardump(module, uuid, title, tags, data) print("[ONLINE PROFILE] saved character ", uuid) end +function _M:setSaveID(module, uuid, savename, md5) + if not self.auth or not self.hash_valid or not md5 then return end + core.profile.pushOrder(table.serialize{o="SaveMD5", + module=module, + uuid=uuid, + savename=savename, + md5=md5, + }) + print("[ONLINE PROFILE] saved character md5", uuid, savename, md5) +end + +function _M:checkSaveID(module, uuid, savename, md5) + if not self.auth or not self.hash_valid or not md5 then return function() return false end end + core.profile.pushOrder(table.serialize{o="CheckSaveMD5", + module=module, + uuid=uuid, + savename=savename, + md5=md5, + }) + print("[ONLINE PROFILE] checking character md5", uuid, savename, md5) + return function() + local ok = false + self:waitEvent("CheckSaveMD5", function(e) + if e.savename == savename and e.ok then ok = true end + end, 30000) + return ok + end +end + function _M:currentCharacter(module, title, uuid) if not self.auth then return end core.profile.pushOrder(table.serialize{o="CurrentCharacter", diff --git a/game/engines/default/engine/Savefile.lua b/game/engines/default/engine/Savefile.lua index fafa3706386b8d3458e7f1ca95b59003c6d52eff..8c57a45f96cc1b53765e5d5c32eef66b772e9591 100644 --- a/game/engines/default/engine/Savefile.lua +++ b/game/engines/default/engine/Savefile.lua @@ -32,6 +32,7 @@ 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 @@ -57,6 +58,10 @@ function _M:setCurrent(save) _M.current_save = save end +function _M:setSaveMD5Type(type) + self.md5_types[type] = true +end + --- Finishes up a savefile -- Always call it once done function _M:close() @@ -142,6 +147,8 @@ function _M:saveWorld(world, no_dialog) fs.delete(self.save_dir..self:nameSaveWorld(world)) fs.rename(self.save_dir..self:nameSaveWorld(world)..".tmp", self.save_dir..self:nameSaveWorld(world)) + self:md5Upload("world", self:nameSaveWorld(world)) + if not no_dialog then popup:done() end end @@ -211,6 +218,8 @@ function _M:saveGame(game, no_dialog) fs.delete(self.save_dir..self:nameSaveGame(game)) fs.rename(self.save_dir..self:nameSaveGame(game)..".tmp", self.save_dir..self:nameSaveGame(game)) + self:md5Upload("game", self:nameSaveGame(game)) + 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)) @@ -279,6 +288,8 @@ function _M:saveZone(zone, no_dialog) fs.delete(self.save_dir..self:nameSaveZone(zone)) fs.rename(self.save_dir..self:nameSaveZone(zone)..".tmp", self.save_dir..self:nameSaveZone(zone)) + self:md5Upload("zone", self:nameSaveZone(zone)) + if not no_dialog then popup:done() end end @@ -308,6 +319,8 @@ function _M:saveLevel(level, no_dialog) fs.delete(self.save_dir..self:nameSaveLevel(level)) fs.rename(self.save_dir..self:nameSaveLevel(level)..".tmp", self.save_dir..self:nameSaveLevel(level)) + self:md5Upload("level", self:nameSaveLevel(level)) + if not no_dialog then popup:done() end end @@ -337,6 +350,8 @@ function _M:saveEntity(e, no_dialog) fs.delete(self.save_dir..self:nameSaveEntity(e)) fs.rename(self.save_dir..self:nameSaveEntity(e)..".tmp", self.save_dir..self:nameSaveEntity(e)) + self:md5Upload("entity", self:nameSaveEntity(e)) + if not no_dialog then popup:done() end end @@ -380,6 +395,8 @@ function _M:loadWorld() 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...") @@ -395,6 +412,9 @@ function _M:loadWorld() 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 @@ -430,6 +450,8 @@ function _M:loadGame() 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 @@ -440,6 +462,9 @@ function _M:loadGame() 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 @@ -469,6 +494,8 @@ function _M:loadZone(zone) 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") @@ -490,6 +517,9 @@ function _M:loadZone(zone) 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() @@ -502,6 +532,8 @@ function _M:loadLevel(zone, level) 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") @@ -522,6 +554,9 @@ function _M:loadLevel(zone, level) 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) @@ -533,6 +568,8 @@ function _M:loadEntity(name) 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") @@ -553,6 +590,9 @@ function _M:loadEntity(name) 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) @@ -563,3 +603,52 @@ end function _M:check() return fs.exists(self.save_dir..self:nameLoadGame()) end + +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, f) + profile:setSaveID(game.__mod_info.short_name, uuid, f, m) +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 + +function _M:badMD5Load() + if game.onBadMD5Load then game:onBadMD5Load() end + game.bad_md5_loaded = true +end diff --git a/game/engines/default/engine/interface/PlayerDumpJSON.lua b/game/engines/default/engine/interface/PlayerDumpJSON.lua index 54576c01c62b5daf63384be6eaee1fc1965c8e67..58a74fe09d8350d4c722b0b929e276ff67e394b3 100644 --- a/game/engines/default/engine/interface/PlayerDumpJSON.lua +++ b/game/engines/default/engine/interface/PlayerDumpJSON.lua @@ -28,6 +28,7 @@ allow_late_uuid = false --- Register the character on te4.org and return a UUID for it function _M:getUUID() + if self.__te4_uuid then return self.__te4_uuid end local uuid = profile:registerNewCharacter(game.__mod_info.short_name) if uuid then self.__te4_uuid = uuid diff --git a/game/modules/tome/load.lua b/game/modules/tome/load.lua index 5e5369511af5686e25dfd8b22d5c2d648794b5e9..282b289974398c277ab7c212a013667dcb728f87 100644 --- a/game/modules/tome/load.lua +++ b/game/modules/tome/load.lua @@ -25,6 +25,7 @@ local Entity = require "engine.Entity" Entity.ascii_outline = {x=2, y=2, r=0, g=0, b=0, a=0.8} -- This file loads the game module, and loads data +local Savefile = require "engine.Savefile" local KeyBind = require "engine.KeyBind" local DamageType = require "engine.DamageType" local Faction = require "engine.Faction" @@ -48,6 +49,10 @@ local PlayerHotkeys = require "engine.interface.PlayerHotkeys" local Quest = require "engine.Quest" local UIBase = require "engine.ui.Base" +Savefile:setSaveMD5Type("game") +Savefile:setSaveMD5Type("level") +Savefile:setSaveMD5Type("zone") + -- Init settings config.settings.tome = config.settings.tome or {} profile.mod.allow_build = profile.mod.allow_build or {} diff --git a/game/profile-thread/Client.lua b/game/profile-thread/Client.lua index 26a6307ed096ba4cfb10d8b99686506219659c44..d3c4342016e7d7245466443ed670bddaa3d4028c 100644 --- a/game/profile-thread/Client.lua +++ b/game/profile-thread/Client.lua @@ -365,6 +365,19 @@ function _M:orderGetCharball(o) end end +function _M:orderSaveMD5(o) + self:command("CHAR", "SETSAVEID", o.md5, o.uuid, o.module, o.savename) +end + +function _M:orderCheckSaveMD5(o) + self:command("CHAR", "CHECKSAVEID", o.md5, o.uuid, o.module, o.savename) + if self:read("200") then + cprofile.pushEvent(string.format("e='CheckSaveMD5' ok=true savename=%q", o.savename)) + else + cprofile.pushEvent(string.format("e='CheckSaveMD5' ok=false savename=%q", o.savename)) + end +end + function _M:orderCurrentCharacter(o) self:command("CHAR", "CUR", table.serialize(o)) self.cur_char = o diff --git a/ideas/todo b/ideas/todo index 12fe9ac36dde778cdbe28739fdebd1fcbd87cb9a..3fc8b1b3990a17260315ecc3f988be427b437765 100644 --- a/ideas/todo +++ b/ideas/todo @@ -12,4 +12,3 @@ - gust of wind trap (for X turn a strong wind blows in a random direction, force-moving things * slaves in the ring of blood zone should be freed once slaver is dead = achievement for many * sub-tile entities, like the spiders for the arachnomancer: split a tile in 16 (4x4) -* zone/level save => trigger a game save