From 0cf5e53094b927f31e4886f80cb985deb875deb6 Mon Sep 17 00:00:00 2001
From: dg <dg@51575b47-30f0-44d4-a5cc-537603b46e54>
Date: Fri, 1 Jul 2011 22:26:17 +0000
Subject: [PATCH] Improved online profile code, it should be more efficient and
 more controlled. Serverside storage changed completly, it will allow for much
 nicer server features. Due to the online profile storage change localy stored
 achievements will be lost, if you use an online profile they should come back
 correctly. When b29 is released the new server code will switch to prod, all
 previous betas will continue to sync but it will not update data seen by the
 server anymore and >=b29 versions wont see it. Basically: upgrade

git-svn-id: http://svn.net-core.org/repos/t-engine4@3770 51575b47-30f0-44d4-a5cc-537603b46e54
---
 game/engines/default/engine/Module.lua        |  11 +-
 game/engines/default/engine/PlayerProfile.lua | 223 +++++++++++-------
 .../engine/interface/WorldAchievements.lua    |   2 +-
 game/engines/default/engine/utils.lua         |   4 +-
 game/engines/default/modules/boot/init.lua    |   4 +-
 game/modules/tome/class/Actor.lua             |   2 +-
 game/modules/tome/class/Game.lua              |   5 +-
 game/modules/tome/class/Object.lua            |   2 +-
 .../tome/class/interface/PlayerStats.lua      |  33 +--
 .../tome/data/general/objects/egos/boots.lua  |  23 +-
 .../tome/data/zones/town-angolwen/npcs.lua    |   1 -
 game/modules/tome/init.lua                    |  12 +
 game/profile-thread/Client.lua                |  36 ++-
 ideas/quests.ods                              | Bin 12301 -> 12568 bytes
 src/profile.c                                 |   2 +
 15 files changed, 217 insertions(+), 143 deletions(-)

diff --git a/game/engines/default/engine/Module.lua b/game/engines/default/engine/Module.lua
index a2fed66a42..c338eed101 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 2606d4ae17..b844bddd82 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 6fd3a68ae7..1cb397e117 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 ce8e002d30..26a17fc6cf 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 6c74d50df2..1dd00d698b 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 255f9b02f7..1567c6a7e2 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 f7beb01196..a8370de221 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 2c289dda29..7788808e40 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 2ebf2ad857..aedea4c56c 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 dc29aec601..667a71de9f 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 867a79d2f3..085646f185 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 5b649c668b..a8a3094a35 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 ed2d011cf4..a79d0987ef 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
GIT binary patch
delta 9886
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

delta 9572
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

diff --git a/src/profile.c b/src/profile.c
index 3c796a1a95..7afd243618 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
-- 
GitLab