Skip to content
Snippets Groups Projects
Forked from tome / Tales of MajEyal
7867 commits behind the upstream repository.
PlayerProfile.lua 26.96 KiB
-- TE4 - T-Engine 4
-- Copyright (C) 2009, 2010, 2011, 2012, 2013 Nicolas Casalini
--
-- 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"
local http = require "socket.http"
local url = require "socket.url"
local ltn12 = require "ltn12"
local Dialog = require "engine.ui.Dialog"
local UserChat = require "engine.UserChat"
require "Json2"

------------------------------------------------------------
-- some simple serialization stuff
------------------------------------------------------------
local function basicSerialize(o)
	if type(o) == "number" or type(o) == "boolean" then
		return tostring(o)
	elseif type(o) == "function" then
		return string.format("loadstring(%q)", string.dump(o))
	else   -- assume it is a string
		return string.format("%q", o)
	end
end

local function serialize_data(outf, name, value, saved, filter, allow, savefile, force)
	saved = saved or {}       -- initial value
	outf(name, " = ")
	if type(value) == "number" or type(value) == "string" or type(value) == "boolean" or type(value) == "function" then
		outf(basicSerialize(value), "\n")
	elseif type(value) == "table" then
			saved[value] = name   -- save name for next time
			outf("{}\n")     -- create a new table

			for k,v in pairs(value) do      -- save its fields
				local fieldname
				fieldname = string.format("%s[%s]", name, basicSerialize(k))
				serialize_data(outf, fieldname, v, saved, {new=true}, false, savefile, false)
			end
	else
		error("cannot save a " .. type(value) .. " ("..name..")")
	end
end

local function serialize(data)
	local tbl = {}
	local outf = function(...) for i,str in ipairs{...} do table.insert(tbl, str) end end
	for k, e in pairs(data) do
		serialize_data(outf, tostring(k), e)
	end
	return table.concat(tbl)
end
------------------------------------------------------------

--- Handles the player profile, possibly online
module(..., package.seeall, class.make)

function _M:init()
	self.chat = UserChat.new()
	self.dlc_files = {classes={}, files={}}
	self.saved_events = {}
	self.generic = {}
	self.modules = {}
	self.evt_cbs = {}
	self.stats_fields = {}
	local checkstats = function(self, field) return self.stats_fields[field] end
	self.config_settings =
	{
		[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" } },
		["^achievements$"] = { invalid = { read={offline=true,online=true}, write="offline" }, valid = { read={online=true}, write="online" } },
		["^donations$"] = { invalid = { read={offline=true}, write="offline" }, valid = { read={offline=true}, write="offline" } },
	}
	self.auth = false
end

function _M:start()
	self:funFactsGrab()
	self:loadGenericProfile()

	if self.generic.online and self.generic.online.login and self.generic.online.pass then
		self.login = self.generic.online.login
		self.pass = self.generic.online.pass
		self:tryAuth()
		self:waitFirstAuth()
	elseif core.steam and self.generic.onlinesteam and self.generic.onlinesteam.autolog then
		local ticket = core.steam.sessionTicket()
		if ticket then
			self.steam_token = ticket:toHex()
			self:tryAuth()
			self:waitFirstAuth()
		end
	end
end

function _M:addStatFields(...)
	for i, f in ipairs{...} do
		self.stats_fields[f] = true
	end
end

function _M:loadData(f, where)
	setfenv(f, where)
	local ok, err = pcall(f)
	if not ok and err then print("Error executing data", err) end
end

function _M:mountProfile(online, module)
	-- Create the directory if needed
	local restore = fs.getWritePath()
	fs.setWritePath(engine.homepath)
	fs.mkdir(string.format("/profiles/%s/generic/", online and "online" or "offline"))
	if module then fs.mkdir(string.format("/profiles/%s/modules/%s", online and "online" or "offline", module)) end

	local path = engine.homepath.."/profiles/"..(online and "online" or "offline")
	fs.mount(path, "/current-profile")
	print("[PROFILE] mounted ", online and "online" or "offline", "on /current-profile")
	fs.setWritePath(path)

	return restore
end
function _M:umountProfile(online, pop)
	local path = engine.homepath.."/profiles/"..(online and "online" or "offline")
	fs.umount(path)
	print("[PROFILE] unmounted ", online and "online" or "offline", "from /current-profile")

	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 },
	onlinesteam = {nosync=true, {autolog="boolean"}, receive=function(data, save) save.autolog = data.autolog 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()
	-- Delay when we are currently saving
	if savefile_pipe and savefile_pipe.saving then savefile_pipe:pushGeneric("loadGenericProfile", function() self:loadGenericProfile() end) return end

	local pop = self:mountProfile(true)
	local d = "/current-profile/generic/"
	for i, file in ipairs(fs.list(d)) do
		if file:find(".profile$") then
			local f, err = loadfile(d..file)
			if not f and err then
				print("Error loading data profile", file, err)
			else
				local field = file:gsub(".profile$", "")
				self.generic[field] = self.generic[field] or {}
				self:loadData(f, self.generic[field])
			end
		end
	end
	self:umountProfile(true, pop)
end

--- Check if we can load this field from this profile
function _M:filterLoadData(online, field)
	local ok = false
	for f, conf in pairs(self.config_settings) do
		local try = false
		if type(f) == "string" then try = field:find(f)
		elseif type(f) == "function" then try = f(self, field) end
		if try then
			local c
			if self.hash_valid then c = conf.valid
			else c = conf.invalid
			end
			if not c then break end

			c = c.read
			if not c then break end

			if online and c.online then ok = true
			elseif not online and c.offline then ok = true
			end
			break
		end
	end
	print("[PROFILE] filtering load of ", field, " from profile ", online and "online" or "offline", "=>", ok and "allowed" or "disallowed")
	return ok
end

--- Return if we should save this field in the online or offline profile
function _M:filterSaveData(field)
	local online = false
	for f, conf in pairs(self.config_settings) do
		local try = false
		if type(f) == "string" then try = field:find(f)
		elseif type(f) == "function" then try = f(self, field) end
		if try then
			local c
			if self.hash_valid then c = conf.valid
			else c = conf.invalid
			end
			if not c then break end

			c = c.write
			if not c then break end

			if c == "online" then online = true else online = false end
			break
		end
	end
	print("[PROFILE] filtering save of ", field, " to profile ", online and "online" or "offline")
	return online
end

--- Loads profile module profile from disk
function _M:loadModuleProfile(short_name, mod_def)
	if short_name == "boot" then return end

	-- Delay when we are currently saving
	if savefile_pipe and savefile_pipe.saving then savefile_pipe:pushGeneric("loadModuleProfile", function() self:loadModuleProfile(short_name) end) return end

	local function load(online)
		local pop = self:mountProfile(online, short_name)
		local d = "/current-profile/modules/"..short_name.."/"
		self.modules[short_name] = self.modules[short_name] or {}
		for i, file in ipairs(fs.list(d)) do
			if file:find(".profile$") then
				local field = file:gsub(".profile$", "")

				if self:filterLoadData(online, field) then
					local f, err = loadfile(d..file)
					if not f and err then
						print("Error loading data profile", file, err)
					else
						self.modules[short_name][field] = self.modules[short_name][field] or {}
						self:loadData(f, self.modules[short_name][field])
					end
				end
			end
		end
		self:umountProfile(online, pop)
	end

	load(false) -- Load from offline profile
	load(true) -- Load from online profile

	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, nowrite)
	-- Delay when we are currently saving
	if not profile then return end
	if savefile_pipe and savefile_pipe.saving then savefile_pipe:pushGeneric("saveGenericProfile", function() self:saveGenericProfile(name, data, nosync) end) return end

	if not generic_profile_defs[name] then print("[PROFILE] refusing unknown generic data", name) 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)

	if not nowrite then
		local pop = self:mountProfile(true)
		local f = fs.open("/generic/"..name..".profile", "w")
		if f then
			f:write(serialize(dataenv))
			f:close()
		end
		self:umountProfile(true, pop)
	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, 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, nosync) end) return end

	local module = self.mod_name

	-- Check for readability
	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")
		if f then
			f:write(serialize(dataenv))
			f:close()
		end
		self:umountProfile(online, pop)
	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
		self:saveGenericProfile("firstrun", {firstrun=os.time()})
	end
	return result
end

function _M:performlogin(login, pass)
	self.login=login
	self.pass=pass
	print("[ONLINE PROFILE] attempting log in ", self.login)
	self.auth_tried = nil
	self:tryAuth()
	self:waitFirstAuth()
	if (profile.auth) then
		self:saveGenericProfile("online", {login=login, pass=pass})
		self:getConfigs("generic")
		self:syncOnline("generic")
	end
end

function _M:performloginSteam(token, name, email)
	self.steam_token = token
	self.steam_token_name = name
	if email then self.steam_token_email = email end
	print("[ONLINE PROFILE] attempting log in steam", token)
	self.auth_tried = nil
	self:tryAuth()
	self:waitFirstAuth()
	if (profile.auth) then
		self:saveGenericProfile("onlinesteam", {autolog=true})
		self:getConfigs("generic")
		self:syncOnline("generic")
	end
end

-----------------------------------------------------------------------
-- Events from the profile thread
-----------------------------------------------------------------------

function _M:popEvent(specific)
	if not specific then
		if #self.saved_events > 0 then return table.remove(self.saved_events, 1) end
		return core.profile.popEvent()
	else
		for i, evt in ipairs(self.saved_events) do
			if evt.e == specific then return table.remove(self.saved_events, i) end
		end
		local evt = core.profile.popEvent()
		if evt then
			if type(evt) == "string" then evt = evt:unserialize() end

			if evt.e == specific then return evt end
			self.saved_events[#self.saved_events+1] = evt
		end
	end
end

function _M:waitEvent(name, cb, wait_max)
	-- Wait anwser, this blocks thegame but cant really be avoided :/
	local stop = false
	local first = true
	local tries = 0
	while not stop do
		if not first then
			if not self.waiting_event_no_redraw then core.display.forceRedraw() end
			core.game.sleep(50)
		end
		local evt = self:popEvent(name)
		while evt do
			if type(game) == "table" then evt = game:handleProfileEvent(evt)
			else evt = self:handleEvent(evt) end
--			print("==== waiting event", name, evt.e)
			if evt.e == name then
				stop = true
				cb(evt)
				break
			end
			evt = self:popEvent(name)
		end
		first = false
		tries = tries + 1
		if wait_max and tries * 50 > wait_max then break end
	end
end

function _M:noMoreAuthWait()
	self.no_more_wait_auth = true
end

function _M:waitFirstAuth(timeout)
	if self.no_more_wait_auth then return end
	if self.auth_tried and self.auth_tried >= 1 then return end
	if not self.waiting_auth then return end
	print("[PROFILE] waiting for first auth")
	local first = true
	timeout = timeout or 60
	while self.waiting_auth and timeout > 0 do
		if not first then
			if not self.waiting_auth_no_redraw then core.display.forceRedraw() end
			core.game.sleep(50)
		end
		local evt = self:popEvent()
		while evt do
			if type(game) == "table" then game:handleProfileEvent(evt)
			else self:handleEvent(evt) end
			if not self.waiting_auth then break end
			evt = self:popEvent()
		end
		first = false
		timeout = timeout - 1
	end
end

function _M:eventAuth(e)
	self.waiting_auth = false
	self.auth_tried = (self.auth_tried or 0) + 1
	if e.ok then
		self.auth = e.ok:unserialize()
		print("[PROFILE] Main thread got authed", self.auth.name)
		self:getConfigs("generic", function(e) self:syncOnline(e.module) end)
	else
		self.auth_last_error = e.reason or "unknown"
	end
end

function _M:eventGetNews(e)
	if e.news and self.evt_cbs.GetNews then
		self.evt_cbs.GetNews(e.news:unserialize())
		self.evt_cbs.GetNews = nil
	end
end

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
	self:setConfigsBatch(true)
	for i = 1, #data do
		local val = data[i]

		if module == "generic" then
			self:saveGenericProfile(e.kind, val, true, i < #data)
		else
			self:saveModuleProfile(e.kind, val, true, i < #data)
		end
	end
	self:setConfigsBatch(false)
	if self.evt_cbs.GetConfigs then self.evt_cbs.GetConfigs(e) self.evt_cbs.GetConfigs = nil end
end

function _M:eventPushCode(e)
	local f, err = loadstring(e.code)
	if not f then
--		core.profile.pushOrder("o='GetNews'")
	else
		local ok, err = pcall(f)
		if config.settings.cheat then print(ok, err) end
	end
end

function _M:eventChat(e)
	self.chat:event(e)
end

function _M:eventConnected(e)
	if game and type(game) == "table" and game.log then game.log("#YELLOW#Connection to online server established.") end
end

function _M:eventDisconnected(e)
	if game and type(game) == "table" and game.log then game.log("#YELLOW#Connection to online server lost, trying to reconnect.") end
end

function _M:eventFunFacts(e)
	if e.data then
		self.funfacts = zlib.decompress(e.data):unserialize()
	end
end

--- Got an event from the profile thread
function _M:handleEvent(e)
	if type(e) == "string" then e = e:unserialize() end
	if not e then return end
	if self["event"..e.e] then self["event"..e.e](self, e) end
	return e
end

-----------------------------------------------------------------------
-- Orders for the profile thread
-----------------------------------------------------------------------

function _M:getNews(callback)
	print("[ONLINE PROFILE] get news")
	self.evt_cbs.GetNews = callback
	core.profile.pushOrder("o='GetNews'")
end
function _M:tryAuth()
	print("[ONLINE PROFILE] auth")
	self.auth_last_error = nil
	if self.steam_token then
		core.profile.pushOrder(table.serialize{o="SteamLogin", token=self.steam_token, name=self.steam_token_name, email=self.steam_token_email})
	else
		core.profile.pushOrder(table.serialize{o="Login", l=self.login, p=self.pass})
	end
	self.waiting_auth = true
end

function _M:logOut()
	core.profile.pushOrder(table.serialize{o="Logoff"})
	profile.generic.online = nil
	profile.auth = nil

	local pop = self:mountProfile(true)
	fs.delete("/generic/online.profile")
	fs.delete("/generic/onlinesteam.profile")
	self:umountProfile(true, pop)
end

function _M:getConfigs(module, cb, mod_def)
	self:waitFirstAuth()
	if not self.auth then return end
	self.evt_cbs.GetConfigs = cb
	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 or {}) do
			if not _.no_sync then
				core.profile.pushOrder(table.serialize{o="GetConfigs", module=module, kind=k})
			end
		end
	end
end

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 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, 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

	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 or {}) 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
	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
	core.profile.pushOrder(table.serialize{o="CheckModuleHash", module=module, md5=md5})

	self:waitEvent("CheckModuleHash", function(e) ok = e.ok end, 10000)

	if not ok then return nil, "bad game version" end
	print("[ONLINE PROFILE] module hash is valid")
	self.hash_valid = true
	return true
end

function _M:checkAddonHash(module, addon, md5)
	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
	core.profile.pushOrder(table.serialize{o="CheckAddonHash", module=module, addon=addon, md5=md5})

	self:waitEvent("CheckAddonHash", function(e) ok = e.ok end, 10000)

	if not ok then return nil, "bad game addon version" end
	print("[ONLINE PROFILE] addon hash is valid")
	return true
end

function _M:sendError(what, err)
	print("[ONLINE PROFILE] sending error")
	core.profile.pushOrder(table.serialize{o="SendError", login=self.login, what=what, err=err, module=game.__mod_info.short_name, version=game.__mod_info.version_name})
end

function _M:registerNewCharacter(module)
	if not self.auth or not self.hash_valid then return end
	local dialog = Dialog:simpleWaiter("Registering character", "Character is being registered on http://te4.org/")
	core.display.forceRedraw()

	core.profile.pushOrder(table.serialize{o="RegisterNewCharacter", module=module})
	local uuid = nil
	self:waitEvent("RegisterNewCharacter", function(e) uuid = e.uuid end, 10000)

	dialog:done()
	if not uuid then return end
	print("[ONLINE PROFILE] new character UUID ", uuid)
	return uuid
end

function _M:getCharball(id_profile, uuid)
	if not self.auth then return end
	local dialog = Dialog:simpleWaiter("Retrieving data from the server", "Retrieving...")
	core.display.forceRedraw()

	local data = nil
	core.profile.pushOrder(table.serialize{o="GetCharball", module=game.__mod_info.short_name, uuid=uuid, id_profile=id_profile})
	self:waitEvent("GetCharball", function(e) data = e.data end, 30000)

	dialog:done()
	if not data then return end
	return data
end

function _M:getDLCD(name, version, file)
	if not self.auth then return end
	local data = nil
	core.profile.pushOrder(table.serialize{o="GetDLCD", name=name, version=version, file=file})
	self:waitEvent("GetDLCD", function(e) data = e.data end, 30000)
	if not data then 
		print("DLCD for", name, version, file, "got a result of size0")
		return "" 
	end
	print("DLCD for", name, version, file, "got a result of size", (data or ""):len())
	return (data:len() > 0) and zlib.decompress(data) or data
end

function _M:registerSaveCharball(module, uuid, data)
	if not self.auth or not self.hash_valid then return end
	core.profile.pushOrder(table.serialize{o="SaveCharball",
		module=module,
		uuid=uuid,
		data=data,
	})
	print("[ONLINE PROFILE] saved character charball", uuid)
end

function _M:registerSaveChardump(module, uuid, title, tags, data)
	if not self.auth or not self.hash_valid then return end
	core.profile.pushOrder(table.serialize{o="SaveChardump",
		module=module,
		uuid=uuid,
		data=data,
		metadata=table.serialize{tags=tags, title=title},
	})
	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
]]
	return function() return true end
end

function _M:currentCharacter(module, title, uuid)
	if not self.auth then return end
	core.profile.pushOrder(table.serialize{o="CurrentCharacter",
		module=module,
		mod_short=(game and type(game)=="table") and game.__mod_info.short_name or "unknown",
		title=title,
		valid=self.hash_valid,
		uuid=uuid,
	})
	print("[ONLINE PROFILE] current character ", title)
end

function _M:newProfile(Login, Name, Password, Email)
	print("[ONLINE PROFILE] profile options ", Login, Email, Name)

	core.profile.pushOrder(table.serialize{o="NewProfile2", login=Login, email=Email, name=Name, pass=Password})
	local id = nil
	self:waitEvent("NewProfile2", function(e) id = e.uid end)

	if not id then print("[ONLINE PROFILE] could not create") return end
	print("[ONLINE PROFILE] profile id ", id)
	self:performlogin(Login, Password)
end

function _M:entityVaultPoke(module, kind, name, desc, data)
	if not data then return end
	if not self.auth then return end
	core.profile.pushOrder(table.serialize{o="EntityPoke",
		module=module,
		kind=kind,
		name=name,
		desc=desc,
		data=data,
	})
	print("[ONLINE PROFILE] poke entity vault", module, kind, name)
end

function _M:entityVaultPeek(module, kind, id)
	if not id then return end
	if not self.auth then return end
	core.profile.pushOrder(table.serialize{o="EntityPeek",
		module=module,
		kind=kind,
		id=id,
	})
	print("[ONLINE PROFILE] peek entity vault", module, kind, id)
end

function _M:entityVaultEmpty(module, kind, id)
	if not id then return end
	if not self.auth then return end
	core.profile.pushOrder(table.serialize{o="EntityEmpty",
		module=module,
		kind=kind,
		id=id,
	})
	print("[ONLINE PROFILE] empty entity vault", module, kind, id)
end

function _M:entityVaultInfos(module, kind)
	if not self.auth then return end
	core.profile.pushOrder(table.serialize{o="EntityInfos",
		module=module,
		kind=kind,
	})
	print("[ONLINE PROFILE] list entity vault", module, kind)
end

function _M:funFactsGrab(module)
	core.profile.pushOrder(table.serialize{o="FunFactsGrab", module=module})
	print("[ONLINE PROFILE] fun facts", module)
end

function _M:isDonator(s)
	s = s or 1
	if core.steam then return true end
	if not self.auth or not tonumber(self.auth.donated) or tonumber(self.auth.donated) < s then return false else return true end
end