diff --git a/game/engines/default/engine/Module.lua b/game/engines/default/engine/Module.lua
index a2fed66a42b5adc55f03c7987663159f8653eb2e..c338eed1013c8b3d728d2387676f4e43eac393e1 100644
--- a/game/engines/default/engine/Module.lua
+++ b/game/engines/default/engine/Module.lua
@@ -246,11 +246,6 @@ function _M:instanciate(mod, name, new_game, no_reboot)
 
 	mod.version_name = ("%s-%d.%d.%d"):format(mod.short_name, mod.version[1], mod.version[2], mod.version[3])
 
-	profile.generic.modules_loaded = profile.generic.modules_loaded or {}
-	profile.generic.modules_loaded[mod.short_name] = (profile.generic.modules_loaded[mod.short_name] or 0) + 1
-
-	profile:saveGenericProfile("modules_loaded", profile.generic.modules_loaded)
-
 	-- Turn based by default
 	core.game.setRealtime(0)
 
@@ -290,7 +285,8 @@ function _M:instanciate(mod, name, new_game, no_reboot)
 	end
 
 	profile:addStatFields(unpack(mod.profile_stats_fields or {}))
-	profile:loadModuleProfile(mod.short_name)
+	profile:setConfigsBatch(true)
+	profile:loadModuleProfile(mod.short_name, mod)
 	profile:currentCharacter(mod.version_string, "game did not tell us")
 
 	-- Init the module code
@@ -339,6 +335,9 @@ function _M:instanciate(mod, name, new_game, no_reboot)
 		end
 	end
 	print("[MODULE LOADER] done loading module", mod.long_name)
+
+	profile:saveGenericProfile("modules_loaded", {name=mod.short_name, nb={"inc", 1}})
+	profile:setConfigsBatch(false)
 end
 
 --- Setup write dir for a module
diff --git a/game/engines/default/engine/PlayerProfile.lua b/game/engines/default/engine/PlayerProfile.lua
index 2606d4ae1782941a5859173112ed875dff92aac8..b844bddd82e5ff8909eb47b63a248eaf690bdb4a 100644
--- a/game/engines/default/engine/PlayerProfile.lua
+++ b/game/engines/default/engine/PlayerProfile.lua
@@ -83,7 +83,7 @@ function _M:init()
 	{
 		[checkstats]     = { invalid = { read={online=true}, write="online" }, valid = { read={online=true}, write="online" } },
 		["^allow_build$"] = { invalid = { read={offline=true,online=true}, write="offline" }, valid = { read={offline=true,online=true}, write="online" } },
-		["^achievement%..*$"] = { invalid = { read={offline=true,online=true}, write="offline" }, valid = { read={online=true}, write="online" } },
+		["^achievements$"] = { invalid = { read={offline=true,online=true}, write="offline" }, valid = { read={online=true}, write="online" } },
 	}
 
 	self.auth = false
@@ -131,6 +131,14 @@ function _M:umountProfile(online, pop)
 	if pop then fs.setWritePath(pop) end
 end
 
+-- Define the fields that are sync'ed online, and how they are sync'ed
+local generic_profile_defs = {
+	firstrun = {nosync=true, {firstrun="number"}, receive=function(data, save) save.firstrun = data.firstrun end },
+	online = {nosync=true, {login="string:40", pass="string:40"}, receive=function(data, save) save.login = data.login save.pass = data.pass end },
+	modules_played = { {name="index:string:30"}, {time_played="number"}, receive=function(data, save) max_set(save, data.name, data, "time_played") end, export=function(env) for k, v in pairs(env) do add{name=k, time_played=v} end end },
+	modules_loaded = { {name="index:string:30"}, {nb="number"}, receive=function(data, save) max_set(save, data.name, data, "nb") end, export=function(env) for k, v in pairs(env) do add{name=k, nb=v} end end },
+}
+
 --- Loads profile generic profile from disk
 -- Generic profile is always read from the "online" profile
 function _M:loadGenericProfile()
@@ -207,7 +215,7 @@ function _M:filterSaveData(field)
 end
 
 --- Loads profile module profile from disk
-function _M:loadModuleProfile(short_name)
+function _M:loadModuleProfile(short_name, mod_def)
 	if short_name == "boot" then return end
 
 	-- Delay when we are currently saving
@@ -238,84 +246,106 @@ function _M:loadModuleProfile(short_name)
 	load(false) -- Load from offline profile
 	load(true) -- Load from online profile
 
-	self:getConfigs(short_name)
-	self:syncOnline(short_name)
-
 	self.mod = self.modules[short_name]
 	self.mod_name = short_name
+
+	self:getConfigs(short_name, nil, mod_def)
+	self:syncOnline(short_name, mod_def)
 end
 
 --- Saves a profile data
-function _M:saveGenericProfile(name, data, nosync)
+function _M:saveGenericProfile(name, data, no_sync, nowrite)
 	-- Delay when we are currently saving
 	if savefile_pipe and savefile_pipe.saving then savefile_pipe:pushGeneric("saveGenericProfile", function() self:saveGenericProfile(name, data, nosync) end) return end
 
-	data = serialize(data)
+	if not generic_profile_defs[name] then print("[PROFILE] refusing unknown generic data", name) return end
 
-	-- Check for readability
-	local f, err = loadstring(data)
-	if not f then print("[PROFILE] cannot save generic data ", name, data, "it does not parse:") print(err) return end
-	setfenv(f, {})
-	local ok, err = pcall(f)
-	if not ok and err then print("[PROFILE] cannot save generic data", name, data, "it does not parse") print(err) return end
+	profile.generic[name] = profile.generic[name] or {}
+	local dataenv = profile.generic[name]
+	local f = generic_profile_defs[name].receive
+	setfenv(f, {
+		inc_set=function(dataenv, key, data, dkey)
+			local v = data[dkey]
+			if type(v) == "number" then
+			elseif type(v) == "table" and v[1] == "inc" then v = (dataenv[key] or 0) + v[2]
+			end
+			dataenv[key] = v
+			data[dkey] = v
+		end,
+		max_set=function(dataenv, key, data, dkey)
+			local v = data[dkey]
+			if type(v) == "number" then
+			elseif type(v) == "table" and v[1] == "inc" then v = (dataenv[key] or 0) + v[2]
+			end
+			v = math.max(v, dataenv[key] or 0)
+			dataenv[key] = v
+			data[dkey] = v
+		end,
+	})
+	f(data, dataenv)
 
-	local pop = self:mountProfile(true)
-	local f = fs.open("/generic/"..name..".profile", "w")
-	f:write(data)
-	f:close()
-	self:umountProfile(true, pop)
+	if not nowrite then
+		local pop = self:mountProfile(true)
+		local f = fs.open("/generic/"..name..".profile", "w")
+		f:write(serialize(dataenv))
+		f:close()
+		self:umountProfile(true, pop)
+	end
 
-	if not nosync then self:setConfigs("generic", name, data) end
+	if not nosync and not generic_profile_defs[name].no_sync then self:setConfigs("generic", name, data) end
 end
 
 --- Saves a module profile data
-function _M:saveModuleProfile(name, data, module, nosync)
+function _M:saveModuleProfile(name, data, nosync, nowrite)
 	if module == "boot" then return end
+	if not game or not game.__mod_info.profile_defs then return end
 
 	-- Delay when we are currently saving
-	if savefile_pipe and savefile_pipe.saving then savefile_pipe:pushGeneric("saveModuleProfile", function() self:saveModuleProfile(name, data, module, nosync) end) return end
+	if savefile_pipe and savefile_pipe.saving then savefile_pipe:pushGeneric("saveModuleProfile", function() self:saveModuleProfile(name, data, nosync) end) return end
 
-	data = serialize(data)
-	module = module or self.mod_name
+	local module = self.mod_name
 
 	-- Check for readability
-	local f, err = loadstring(data)
-	if not f then print("[PROFILE] cannot save module data ", name, data, "it does not parse:") print(err) return end
-	setfenv(f, {})
-	local ok, err = pcall(f)
-	if not ok and err then print("[PROFILE] cannot save module data", name, data, "it does not parse") print(err) return end
-
-	local online = self:filterSaveData(name)
-	local pop = self:mountProfile(online, module)
---[[
-	local path = "current-profile/modules/"..module.."/"..name..".profile"
-	local f, msg = fs.open(path, "w")
-	print("[PROFILE] search path: ")
-	table.foreach(fs.getSearchPath(), print)
-	print("[PROFILE] path: ", path)
-	print("[PROFILE] real path: ", fs.getRealPath(path), "::", fs.exists(path) and "exists" or "does not exist")
-	print("[PROFILE] write path is: ", fs.getWritePath())
-	if f then
-		f:write(data)
+	profile.mod[name] = profile.mod[name] or {}
+	local dataenv = profile.mod[name]
+	local f = game.__mod_info.profile_defs[name].receive
+	setfenv(f, {
+		inc_set=function(dataenv, key, data, dkey)
+			local v = data[dkey]
+			if type(v) == "number" then
+			elseif type(v) == "table" and v[1] == "inc" then v = (dataenv[key] or 0) + v[2]
+			end
+			dataenv[key] = v
+			data[dkey] = v
+		end,
+		max_set=function(dataenv, key, data, dkey)
+			local v = data[dkey]
+			if type(v) == "number" then
+			elseif type(v) == "table" and v[1] == "inc" then v = (dataenv[key] or 0) + v[2]
+			end
+			v = math.max(v, dataenv[key] or 0)
+			dataenv[key] = v
+			data[dkey] = v
+		end,
+	})
+	f(data, dataenv)
+
+	if not nowrite then
+		local online = self:filterSaveData(name)
+		local pop = self:mountProfile(online, module)
+		local f = fs.open("/modules/"..module.."/"..name..".profile", "w")
+		f:write(serialize(dataenv))
 		f:close()
-	else
-		print("[PROFILE] physfs error:", msg)
+		self:umountProfile(online, pop)
 	end
-]]
-	local f = fs.open("/modules/"..module.."/"..name..".profile", "w")
-	f:write(data)
-	f:close()
-
-	self:umountProfile(online, pop)
 
-	if not nosync then self:setConfigs(module, name, data) end
+	if not nosync and not game.__mod_info.profile_defs[name].no_sync then self:setConfigs(module, name, data) end
 end
 
 function _M:checkFirstRun()
 	local result = self.generic.firstrun
 	if not result then
-		firstrun = { When=os.time() }
-		self:saveGenericProfile("firstrun", firstrun, false)
+		self:saveGenericProfile("firstrun", {firstrun=os.time()})
 	end
 	return result
 end
@@ -328,8 +358,7 @@ function _M:performlogin(login, pass)
 	self:tryAuth()
 	self:waitFirstAuth()
 	if (profile.auth) then
-		self.generic.online = { login=login, pass=pass }
-		self:saveGenericProfile("online", self.generic.online)
+		self:saveGenericProfile("online", {login=login, pass=pass})
 		self:getConfigs("generic")
 		self:syncOnline("generic")
 	end
@@ -411,24 +440,13 @@ function _M:eventGetConfigs(e)
 	local data = zlib.decompress(e.data):unserialize()
 	local module = e.module
 	if not data then print("[ONLINE PROFILE] get configs") return end
-	for name, val in pairs(data) do
-		print("[ONLINE PROFILE] config ", name)
+	for i = 1, #data do
+		local val = data[i]
 
-		local field = name
-		local f, err = loadstring(val)
-		if not f and err then
-			print("Error loading profile config: ", err)
+		if module == "generic" then
+			self:saveGenericProfile(e.kind, val, true, i < #data)
 		else
-			if module == "generic" then
-				self.generic[field] = self.generic[field] or {}
-				self:loadData(f, self.generic[field])
-				self:saveGenericProfile(field, self.generic[field], true)
-			else
-				self.modules[module] = self.modules[module] or {}
-				self.modules[module][field] = self.modules[module][field] or {}
-				self:loadData(f, self.modules[module][field])
-				self:saveModuleProfile(field, self.modules[module][field], module, true)
-			end
+			self:saveModuleProfile(e.kind, val, true, i < #data)
 		end
 	end
 	if self.evt_cbs.GetConfigs then self.evt_cbs.GetConfigs(e) self.evt_cbs.GetConfigs = nil end
@@ -489,36 +507,81 @@ function _M:logOut()
 	self:umountProfile(true, pop)
 end
 
-function _M:getConfigs(module, cb)
+function _M:getConfigs(module, cb, mod_def)
 	self:waitFirstAuth()
 	if not self.auth then return end
 	self.evt_cbs.GetConfigs = cb
-	core.profile.pushOrder(table.serialize{o="GetConfigs", module=module})
+	if module == "generic" then
+		for k, _ in pairs(generic_profile_defs) do
+			if not _.no_sync then
+				core.profile.pushOrder(table.serialize{o="GetConfigs", module=module, kind=k})
+			end
+		end
+	else
+		for k, _ in pairs((mod_def or game.__mod_info).profile_defs) do
+			if not _.no_sync then
+				core.profile.pushOrder(table.serialize{o="GetConfigs", module=module, kind=k})
+			end
+		end
+	end
 end
 
-function _M:setConfigs(module, name, val)
+function _M:setConfigsBatch(v)
+	core.profile.pushOrder(table.serialize{o="SetConfigsBatch", v=v and true or false})
+end
+
+function _M:setConfigs(module, name, data)
 	self:waitFirstAuth()
 	if not self.auth then return end
 	if name == "online" then return end
-	if type(val) ~= "string" then val = serialize(val) end
-	core.profile.pushOrder(table.serialize{o="SetConfigs", module=module, data=zlib.compress(table.serialize{[name] = val})})
+	if module ~= "generic" then
+		if not game.__mod_info.profile_defs then print("[PROFILE] saving config but no profile defs", module, name) return end
+		if not game.__mod_info.profile_defs[name] then print("[PROFILE] saving config but no profile def kind", module, name) return end
+	else
+		if not generic_profile_defs[name] then print("[PROFILE] saving config but no profile def kind", module, name) return end
+	end
+	core.profile.pushOrder(table.serialize{o="SetConfigs", module=module, kind=name, data=zlib.compress(table.serialize(data))})
 end
 
-function _M:syncOnline(module)
+function _M:syncOnline(module, mod_def)
 	self:waitFirstAuth()
 	if not self.auth then return end
 	local sync = self.generic
 	if module ~= "generic" then sync = self.modules[module] end
 	if not sync then return end
 
-	local data = {}
-	for k, v in pairs(sync) do if k ~= "online" then data[k] = serialize(v) end end
-
-	core.profile.pushOrder(table.serialize{o="SetConfigs", module=module, data=zlib.compress(table.serialize(data))})
+	self:setConfigsBatch(true)
+	if module == "generic" then
+		for k, def in pairs(generic_profile_defs) do
+			if not def.no_sync and def.export and sync[k] then
+				local f = def.export
+				local ret = {}
+				setfenv(f, setmetatable({add=function(d) ret[#ret+1] = d end}, {__index=_G}))
+				f(sync[k])
+				for i, r in ipairs(ret) do
+					core.profile.pushOrder(table.serialize{o="SetConfigs", module=module, kind=k, data=zlib.compress(table.serialize(r))})
+				end
+			end
+		end
+	else
+		for k, def in pairs((mod_def or game.__mod_info).profile_defs) do
+			if not def.no_sync and def.export and sync[k] then
+				local f = def.export
+				local ret = {}
+				setfenv(f, setmetatable({add=function(d) ret[#ret+1] = d end}, {__index=_G}))
+				f(sync[k])
+				for i, r in ipairs(ret) do
+					core.profile.pushOrder(table.serialize{o="SetConfigs", module=module, kind=k, data=zlib.compress(table.serialize(r))})
+				end
+			end
+		end
+	end
+	self:setConfigsBatch(false)
 end
 
 function _M:checkModuleHash(module, md5)
 	self.hash_valid = false
+do self.hash_valid = true return true end
 --	if not self.auth then return nil, "no online profile active" end
 	if config.settings.cheat then return nil, "cheat mode active" end
 	if game and game:isTainted() then return nil, "savefile tainted" end
diff --git a/game/engines/default/engine/interface/WorldAchievements.lua b/game/engines/default/engine/interface/WorldAchievements.lua
index 6fd3a68ae73d6e0df18ac361273d028ecc00bc54..1cb397e11763f77398b12607d115cf6e2ae65e95 100644
--- a/game/engines/default/engine/interface/WorldAchievements.lua
+++ b/game/engines/default/engine/interface/WorldAchievements.lua
@@ -131,7 +131,7 @@ function _M:gainAchievement(id, src, ...)
 	self:gainPersonalAchievement(true, id, src, ...)
 
 	self.achieved[id] = {turn=game.turn, who=self:achievementWho(src), when=os.date("%Y-%m-%d %H:%M:%S")}
-	profile:saveModuleProfile("achievement."..id, self.achieved[id])
+	profile:saveModuleProfile("achievements", {id=id, turn=game.turn, who=self:achievementWho(src), gained_on=os.date("%Y-%m-%d %H:%M:%S")})
 	game.log("#LIGHT_GREEN#New Achievement: %s!", a.name)
 	self:showAchievement("New Achievement: #LIGHT_GREEN#"..a.name, a)
 	profile.chat:achievement(a.name)
diff --git a/game/engines/default/engine/utils.lua b/game/engines/default/engine/utils.lua
index ce8e002d3072bdd4587532b5ade96965820a1114..26a17fc6cf957445a0e0d7e1c2287ab9b1c896d4 100644
--- a/game/engines/default/engine/utils.lua
+++ b/game/engines/default/engine/utils.lua
@@ -1057,9 +1057,7 @@ function util.showMainMenu(no_reboot, reboot_engine, reboot_engine_version, rebo
 
 	if game and type(game) == "table" and game.__session_time_played_start then
 		if game.onDealloc then game:onDealloc() end
-		profile.generic.modules_played = profile.generic.modules_played or {}
-		profile.generic.modules_played[game.__mod_info.short_name] = (profile.generic.modules_played[game.__mod_info.short_name] or 0) + (os.time() - game.__session_time_played_start)
-		profile:saveGenericProfile("modules_played", profile.generic.modules_played)
+		profile:saveGenericProfile("modules_played", {name=game.__mod_info.short_name, time_played={"inc", os.time() - game.__session_time_played_start}})
 	end
 
 	-- Join threads
diff --git a/game/engines/default/modules/boot/init.lua b/game/engines/default/modules/boot/init.lua
index 6c74d50df27023315a434860cecc5f8d82e4c5a0..1dd00d698bed5989e5d79c14020c6d03bd2c491c 100644
--- a/game/engines/default/modules/boot/init.lua
+++ b/game/engines/default/modules/boot/init.lua
@@ -23,8 +23,8 @@ short_name = "boot"
 author = { "DarkGod", "darkgod@te4.org" }
 homepage = "http://te4.org/"
 is_boot = true
-version = {0,9,26}
-engine = {0,9,26,"te4"}
+version = {0,9,29}
+engine = {0,9,29,"te4"}
 description = [[
 Bootmenu!
 ]]
diff --git a/game/modules/tome/class/Actor.lua b/game/modules/tome/class/Actor.lua
index 255f9b02f7c1b1460f346dd72757696a171c02d4..1567c6a7e290dd88da16a47c9326471c30f4ca69 100644
--- a/game/modules/tome/class/Actor.lua
+++ b/game/modules/tome/class/Actor.lua
@@ -1835,7 +1835,7 @@ function _M:onAddObject(o)
 
 	-- Achievement checks
 	if self.player then
-		if o.unique then
+		if o.unique and not o.lore and not o.randart then
 			game.player:registerArtifactsPicked(o)
 		end
 		world:gainAchievement("DEUS_EX_MACHINA", self, o)
diff --git a/game/modules/tome/class/Game.lua b/game/modules/tome/class/Game.lua
index f7beb011960833e1232449100a2dde37f0373859..a8370de221d5a026f6a9b9fb949d6d518f3fed9a 100644
--- a/game/modules/tome/class/Game.lua
+++ b/game/modules/tome/class/Game.lua
@@ -1438,12 +1438,11 @@ function _M:setAllowedBuild(what, notify)
 	-- Do not unlock things in easy mode
 	--if self.difficulty == self.DIFFICULTY_EASY then return end
 
-	profile.mod.allow_build = profile.mod.allow_build or {}
+	profile:saveModuleProfile("allow_build", {name=what})
+
 	if profile.mod.allow_build[what] then return end
 	profile.mod.allow_build[what] = true
 
-	profile:saveModuleProfile("allow_build", profile.mod.allow_build)
-
 	if notify then
 		self.state:checkDonation() -- They gained someting nice, they could be more receptive
 		self:registerDialog(require("mod.dialogs.UnlockDialog").new(what))
diff --git a/game/modules/tome/class/Object.lua b/game/modules/tome/class/Object.lua
index 2c289dda291b02dc3fddf79508a6c61272d9c0c3..7788808e4081f3f21f89df5df00a52421c641de6 100644
--- a/game/modules/tome/class/Object.lua
+++ b/game/modules/tome/class/Object.lua
@@ -981,7 +981,7 @@ function _M:on_identify()
 		game.player:learnLore(self.on_id_lore)
 	end
 	if self.unique and self.desc and not self.no_unique_lore then
-		game.player:additionalLore(self.unique, self:getName(), "artifacts", self.desc)
+		game.player:additionalLore(self.unique, self:getName{no_add_name=true, do_color=false, no_count=true}, "artifacts", self.desc)
 		game.player:learnLore(self.unique)
 	end
 end
diff --git a/game/modules/tome/class/interface/PlayerStats.lua b/game/modules/tome/class/interface/PlayerStats.lua
index 2ebf2ad857d8fdadbed4806d208c818e0556c67b..aedea4c56cfeaf526bfd1c2ccd4d236138986331 100644
--- a/game/modules/tome/class/interface/PlayerStats.lua
+++ b/game/modules/tome/class/interface/PlayerStats.lua
@@ -28,53 +28,34 @@ end
 function _M:registerDeath(src)
 	local pid = self:playerStatGetCharacterIdentifier(game.party:findMember{main=true})
 	local name = src.name
-
-	profile.mod.deaths = profile.mod.deaths or {}
-	profile.mod.deaths.count = (profile.mod.deaths.count or 0) + 1
-
-	profile.mod.deaths.sources = profile.mod.deaths.sources or {}
-	profile.mod.deaths.sources[pid] = profile.mod.deaths.sources[pid] or {}
-	profile.mod.deaths.sources[pid][name] = (profile.mod.deaths.sources[pid][name] or 0) + 1
-	profile:saveModuleProfile("deaths", profile.mod.deaths)
+	profile:saveModuleProfile("deaths", {source=name, cid=pid, nb={"inc",1}})
 end
 
 function _M:registerUniqueKilled(who)
 	local pid = self:playerStatGetCharacterIdentifier(game.party:findMember{main=true})
 
-	profile.mod.uniques = profile.mod.uniques or { uniques={} }
-	profile.mod.uniques.uniques[who.name] = profile.mod.uniques.uniques[who.name] or {}
-	profile.mod.uniques.uniques[who.name][pid] = (profile.mod.uniques.uniques[who.name][pid] or 0) + 1
-	profile:saveModuleProfile("uniques", profile.mod.uniques)
+	profile:saveModuleProfile("uniques", {victim=who.name, cid=pid, nb={"inc",1}})
 end
 
 function _M:registerArtifactsPicked(what)
 	if what.stat_picked_up then return end
 	what.stat_picked_up = true
 	local pid = self:playerStatGetCharacterIdentifier(game.party:findMember{main=true})
-	local name = what:getName{do_color=false, do_count=false, force_id=true}
+	local name = what:getName{do_color=false, do_count=false, force_id=true, no_add_name=true}
 
-	profile.mod.artifacts = profile.mod.artifacts or { artifacts={} }
-	profile.mod.artifacts.artifacts[name] = profile.mod.artifacts.artifacts[name] or {}
-	profile.mod.artifacts.artifacts[name][pid] = (profile.mod.artifacts.artifacts[name][pid] or 0) + 1
-	profile:saveModuleProfile("artifacts", profile.mod.artifacts)
+	profile:saveModuleProfile("artifacts", {name=name, cid=pid, nb={"inc",1}})
 end
 
 function _M:registerCharacterPlayed()
 	local pid = self:playerStatGetCharacterIdentifier(game.party:findMember{main=true})
 
-	profile.mod.characters = profile.mod.characters or { characters={} }
-	profile.mod.characters.characters[pid] = (profile.mod.characters.characters[pid] or 0) + 1
-	profile:saveModuleProfile("characters", profile.mod.characters)
+	profile:saveModuleProfile("characters", {cid=pid, nb={"inc",1}})
 end
 
 function _M:registerLoreFound(lore)
-	profile.mod.lore = profile.mod.lore or { lore={} }
-	profile.mod.lore.lore[lore] = true
-	profile:saveModuleProfile("lore", profile.mod.lore)
+	profile:saveModuleProfile("lore", {name=lore, nb={"inc",1}})
 end
 
 function _M:registerEscorts(status)
-	profile.mod.escorts = profile.mod.escorts or { saved=0, lost=0, betrayed=0, zigur=0 }
-	profile.mod.escorts[status] = profile.mod.escorts[status] + 1
-	profile:saveModuleProfile("escorts", profile.mod.escorts)
+	profile:saveModuleProfile("escorts", {fate=status, nb={"inc",1}})
 end
diff --git a/game/modules/tome/data/general/objects/egos/boots.lua b/game/modules/tome/data/general/objects/egos/boots.lua
index dc29aec6014b69ca2d74c01ebd0a006dbf07279a..667a71de9fe61e3203a64b0851998c37254fc3fc 100644
--- a/game/modules/tome/data/general/objects/egos/boots.lua
+++ b/game/modules/tome/data/general/objects/egos/boots.lua
@@ -223,7 +223,6 @@ newEntity{
 	rarity = 35,
 	cost = 80,
 	max_power = 80, power_regen = 1,
-	use_talent = { id = Talents.T_DISENGAGE, level = 2, power = 80 },
 	wielder = {
 		resists={
 			[DamageType.NATURE] = resolvers.mbonus_material(20, 10, function(e, v) return 0, -v end),
@@ -234,7 +233,7 @@ newEntity{
 		},
 		pin_immune = resolvers.mbonus_material(50, 40, function(e, v) v=v/100 return 0, v end),
 		combat_spellpower = resolvers.mbonus_material(7, 3),
-	},	
+	},
 }
 
 newEntity{
@@ -251,7 +250,7 @@ newEntity{
 		blind_immune = resolvers.mbonus_material(15, 5, function(e, v) v=v/100 return 0, v end),
 		confusion_immune = resolvers.mbonus_material(15, 5, function(e, v) v=v/100 return 0, v end),
 		disease_immune = resolvers.mbonus_material(15, 5, function(e, v) v=v/100 return 0, v end),
-	},	
+	},
 }
 
 newEntity{
@@ -269,7 +268,7 @@ newEntity{
 			[DamageType.COLD] = resolvers.mbonus_material(10, 5),
 		},
 		combat_armor = resolvers.mbonus_material(7, 3),
-	},	
+	},
 }
 
 newEntity{
@@ -283,7 +282,7 @@ newEntity{
 		max_mana = resolvers.mbonus_material(40, 20),
 		mana_regen = resolvers.mbonus_material(50, 10, function(e, v) v=v/100 return 0, v end),
 		combat_spellcrit = resolvers.mbonus_material(4, 1),
-	},	
+	},
 }
 
 newEntity{
@@ -300,7 +299,7 @@ newEntity{
 		stun_immune = resolvers.mbonus_material(15, 10, function(e, v) v=v/100 return 0, v end),
 		pin_immune = resolvers.mbonus_material(15, 10, function(e, v) v=v/100 return 0, v end),
 		confusion_immune = resolvers.mbonus_material(15, 10, function(e, v) v=v/100 return 0, v end),
-	},	
+	},
 }
 
 newEntity{
@@ -320,7 +319,7 @@ newEntity{
 		pin_immune = resolvers.mbonus_material(15, 10, function(e, v) v=v/100 return 0, v end),
 		poison_immune = resolvers.mbonus_material(15, 10, function(e, v) v=v/100 return 0, -v end),
 		disease_immune = resolvers.mbonus_material(15, 10, function(e, v) v=v/100 return 0, -v end),
-	},	
+	},
 }
 
 newEntity{
@@ -334,7 +333,7 @@ newEntity{
 		resource_leech_chance = resolvers.mbonus_material(10, 5),
 		resource_leech_value = resolvers.mbonus_material(1, 1),
 		max_life = resolvers.mbonus_material(70, 40, function(e, v) return 0, -v end),
-	},	
+	},
 }
 
 newEntity{
@@ -348,10 +347,10 @@ newEntity{
 		disarm_immune = resolvers.mbonus_material(15, 10, function(e, v) v=v/100 return 0, v end),
 		combat_physcrit = resolvers.mbonus_material(4, 1),
 		combat_dam = resolvers.mbonus_material(3, 3),
-		resists_pen = { 
+		resists_pen = {
 			[DamageType.PHYSICAL] = resolvers.mbonus_material(10, 5),
 		},
-	},	
+	},
 }
 
 newEntity{
@@ -367,7 +366,7 @@ newEntity{
 		inc_stats = {
 			[Stats.STAT_MAG] = resolvers.mbonus_material(5, 1),
 		},
-	},	
+	},
 }
 
 newEntity{
@@ -383,5 +382,5 @@ newEntity{
 		combat_mentalresist = resolvers.mbonus_material(7, 1),
 		combat_physresist = resolvers.mbonus_material(7, 1),
 		combat_spellresist = resolvers.mbonus_material(7, 1),
-	},	
+	},
 }
\ No newline at end of file
diff --git a/game/modules/tome/data/zones/town-angolwen/npcs.lua b/game/modules/tome/data/zones/town-angolwen/npcs.lua
index 867a79d2f3b10b41816160e0f789dc3b8c35a10f..085646f185b6d27413157a7a6508205cc617f223 100644
--- a/game/modules/tome/data/zones/town-angolwen/npcs.lua
+++ b/game/modules/tome/data/zones/town-angolwen/npcs.lua
@@ -94,7 +94,6 @@ newEntity{ define_as = "TARELION",
 	level_range = {30, nil}, exp_worth = 2,
 	rank = 4,
 	size_category = 3,
-	female = true,
 	mana_regen = 120,
 	max_mana = 2000,
 	max_life = 350, life_rating = 24, fixed_rating = true,
diff --git a/game/modules/tome/init.lua b/game/modules/tome/init.lua
index 5b649c668b5b8f952b211da7f6692a3ecd7b3bd6..a8a3094a350a14bc0420ff234834b8bf698ac6db 100644
--- a/game/modules/tome/init.lua
+++ b/game/modules/tome/init.lua
@@ -48,3 +48,15 @@ starter = "mod.load"
 profile_stats_fields = {"artifacts", "characters", "deaths", "uniques", "lore", "escorts"}
 allow_userchat = true -- We can talk to the online community
 no_get_name = true -- Name setting for new characters is done by the module itself
+
+-- Define the fields that are sync'ed online, and how they are sync'ed
+profile_defs = {
+	allow_build = { {name="index:string:30"}, receive=function(data, save) save[data.name] = true end, export=function(env) for k, _ in pairs(env) do add{name=k} end end },
+	lore = { {name="index:string:30"}, receive=function(data, save) save.lore = save.lore or {} save.lore[data.name] = true end, export=function(env) for k, v in pairs(env.lore or {}) do add{name=k} end end },
+	escorts = { {fate="index:enum(lost,betrayed,zigur,saved)"}, {nb="number"}, receive=function(data, save) inc_set(save, data.fate, data, "nb") end, export=function(env) for k, v in pairs(env) do add{fate=k, nb=v} end end },
+	artifacts = { {cid="index:string:50"}, {name="index:string:40"}, {nb="number"}, receive=function(data, save) save.artifacts = save.artifacts or {} save.artifacts[data.cid] = save.artifacts[data.cid] or {} inc_set(save.artifacts[data.cid], data.name, data, "nb") end, export=function(env) for cid, d in pairs(env.artifacts or {}) do for name, v in pairs(d) do add{cid=cid, name=name, nb=v} end end end },
+	characters = { {cid="index:string:50"}, {nb="number"}, receive=function(data, save) save.characters = save.characters or {} inc_set(save.characters, data.cid, data, "nb") end, export=function(env) for k, v in pairs(env.characters or {}) do add{cid=k, nb=v} end end },
+	uniques = { {cid="index:string:50"}, {victim="index:string:50"}, {nb="number"}, receive=function(data, save) save.uniques = save.uniques or {} save.uniques[data.cid] = save.uniques[data.cid] or {} inc_set(save.uniques[data.cid], data.victim, data, "nb") end, export=function(env) for cid, d in pairs(env.uniques or {}) do for name, v in pairs(d) do add{cid=cid, victim=name, nb=v} end end end },
+	deaths = { {cid="index:string:50"}, {source="index:string:50"}, {nb="number"}, receive=function(data, save) save.sources = save.sources or {} save.sources[data.cid] = save.sources[data.cid] or {} inc_set(save.sources[data.cid], data.source, data, "nb") end, export=function(env) for cid, d in pairs(env.sources or {}) do for name, v in pairs(d) do add{cid=cid, source=name, nb=v} end end end },
+	achievements = { {id="index:string:40"}, {gained_on="timestamp"}, {who="string:50"}, {turn="number"}, receive=function(data, save) save[data.id] = {who=data.who, when=data.gained_on, turn=data.turn} end, export=function(env) for id, v in pairs(env) do add{id=id, who=v.who, gained_on=v.when, turn=v.turn} end end },
+}
diff --git a/game/profile-thread/Client.lua b/game/profile-thread/Client.lua
index ed2d011cf4f1517721599fde09f6cafbb12ca269..a79d0987efb139232308e8add67ea59546ecb5b6 100644
--- a/game/profile-thread/Client.lua
+++ b/game/profile-thread/Client.lua
@@ -30,7 +30,7 @@ end
 
 function _M:connected()
 	if self.sock then return true end
-	self.sock = socket.connect("te4.org", 2257)
+	self.sock = socket.connect("te4.org", 2259)
 	if not self.sock then return false end
 --	self.sock:settimeout(10)
 	print("[PROFILE] Thread connected to te4.org")
@@ -43,7 +43,7 @@ end
 --- Connects the second tcp channel to receive data
 function _M:connectedPull()
 	if self.psock then return true end
-	self.psock = socket.connect("te4.org", 2258)
+	self.psock = socket.connect("te4.org", 2260)
 	if not self.psock then return false end
 --	self.psock:settimeout(10)
 	print("[PROFILE] Pull socket connected to te4.org")
@@ -256,21 +256,43 @@ end
 
 function _M:orderGetConfigs(o)
 	if not self.auth then return end
-	self:command("GCFS", o.module)
+	self:command("CGET", o.module, o.kind)
 	if self:read("200") then
 		local _, _, size = self.last_line:find("^([0-9]+)")
 		size = tonumber(size)
 		if not size or size < 1 then return end
 		local body = self:receive(size)
-		cprofile.pushEvent(string.format("e='GetConfigs' module=%q data=%q", o.module, body))
+		cprofile.pushEvent(string.format("e='GetConfigs' module=%q kind=%q data=%q", o.module, o.kind, body))
+	end
+end
+
+function _M:orderSetConfigsBatch(o)
+	if not o.v then
+		if not self.setConfigsBatching then return end
+		self.setConfigsBatchingLevel = self.setConfigsBatchingLevel - 1
+		if self.setConfigsBatchingLevel > 0 then return end
+
+		print("[PROFILE THREAD] flushing CSETs")
+
+		local data = zlib.compress(table.serialize(self.setConfigsBatching))
+		self:command("FSET", data:len())
+		if self:read("200") then self.sock:send(data) end
+
+		self.setConfigsBatching = nil
+	else
+		print("[PROFILE THREAD] batching CSETs")
+		self.setConfigsBatching = self.setConfigsBatching or {}
+		self.setConfigsBatchingLevel = (self.setConfigsBatchingLevel or 0) + 1
 	end
 end
 
 function _M:orderSetConfigs(o)
 	if not self.auth then return end
-	self:command("SCFS", o.data:len(), o.module)
-	if self:read("200") then
-		self.sock:send(o.data)
+	if self.setConfigsBatching then
+		self.setConfigsBatching[#self.setConfigsBatching+1] = o
+	else
+		self:command("CSET", o.data:len(), o.module, o.kind)
+		if self:read("200") then self.sock:send(o.data) end
 	end
 end
 
diff --git a/ideas/quests.ods b/ideas/quests.ods
index 267fd2f43118649e1707a2466a6528a062a536b1..9f355f20f4a250d07c5ab1717b42eb549c5008b2 100644
Binary files a/ideas/quests.ods and b/ideas/quests.ods differ
diff --git a/src/profile.c b/src/profile.c
index 3c796a1a95accb5be523aa3d0d5640d08e1df605..7afd2436183639002827882babdf8c378ecd3d30 100644
--- a/src/profile.c
+++ b/src/profile.c
@@ -68,6 +68,7 @@ int pop_order(lua_State *L)
 	if (q)
 	{
 		lua_pushlstring(L, q->payload, q->payload_len);
+//		printf("[profile order POP] %s\n", lua_tostring(L,-1));
 		free(q->payload);
 		free(q);
 	}
@@ -139,6 +140,7 @@ int thread_profile(void *data)
 	luaopen_core(L);
 	luaopen_socket_core(L);
 	luaopen_mime_core(L);
+	luaopen_zlib(L);
 	luaL_openlib(L, "cprofile", threadlib, 0); lua_pop(L, 1);
 
 	// Override "print" if requested