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