Skip to content
Snippets Groups Projects
PlayerProfile.lua 27.3 KiB
Newer Older
dg's avatar
dg committed
-- 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"
dg's avatar
dg committed
local Dialog = require "engine.ui.Dialog"
dg's avatar
dg committed
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)

dg's avatar
dg committed
	self.chat = UserChat.new()
dg's avatar
dg committed
	self.dlc_files = {classes={}, files={}}
	self.saved_events = {}
	self.generic = {}
	self.modules = {}
	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" } },

	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()
DarkGod's avatar
DarkGod committed
	elseif core.steam and self.generic.onlinesteam and self.generic.onlinesteam.autolog then
DarkGod's avatar
DarkGod committed
		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
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")
-- 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 },
DarkGod's avatar
DarkGod committed
	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
	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
	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)
dg's avatar
dg committed
function _M:saveGenericProfile(name, data, nosync, nowrite)
	-- Delay when we are currently saving
dg's avatar
dg committed
	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
	if not nosync and not generic_profile_defs[name].no_sync then self:setConfigs("generic", name, data) end
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
	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
	if not nosync and not game.__mod_info.profile_defs[name].no_sync then self:setConfigs(module, name, data) end
dg's avatar
dg committed
function _M:checkFirstRun()
	local result = self.generic.firstrun
	if not result then
		self:saveGenericProfile("firstrun", {firstrun=os.time()})
dg's avatar
dg committed
	end
	return result
end

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

DarkGod's avatar
DarkGod committed
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

-----------------------------------------------------------------------
dg's avatar
dg committed
-- Events from the profile thread
-----------------------------------------------------------------------

dg's avatar
dg committed
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

dg's avatar
dg committed
function _M:waitEvent(name, cb, wait_max)
	-- Wait anwser, this blocks thegame but cant really be avoided :/
	local stop = false
	local first = true
dg's avatar
dg committed
	local tries = 0
dg's avatar
dg committed
		if not first then
			if not self.waiting_event_no_redraw then core.display.forceRedraw() end
dg's avatar
dg committed
			core.game.sleep(50)
		end
dg's avatar
dg committed
		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
dg's avatar
dg committed
			evt = self:popEvent(name)
dg's avatar
dg committed
		tries = tries + 1
		if wait_max and tries * 50 > wait_max then break end
dg's avatar
dg committed
function _M:noMoreAuthWait()
	self.no_more_wait_auth = true
end

function _M:waitFirstAuth(timeout)
dg's avatar
dg committed
	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
DarkGod's avatar
DarkGod committed
	timeout = timeout or 60
	while self.waiting_auth and timeout > 0 do
dg's avatar
dg committed
		if not first then
			if not self.waiting_auth_no_redraw then core.display.forceRedraw() end
dg's avatar
dg committed
			core.game.sleep(50)
		end
dg's avatar
dg committed
		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
dg's avatar
dg committed
			evt = self:popEvent()
	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)
dg's avatar
dg committed
		self:getConfigs("generic", function(e) self:syncOnline(e.module) end)
DarkGod's avatar
DarkGod committed
	else
		self.auth_last_error = e.reason or "unknown"
dg's avatar
dg committed
	if e.news and self.evt_cbs.GetNews then
		self.evt_cbs.GetNews(e.news:unserialize())
		self.evt_cbs.GetNews = nil
	end
end

dg's avatar
dg committed
function _M:eventGetConfigs(e)
dg's avatar
dg committed
	local data = zlib.decompress(e.data):unserialize()
dg's avatar
dg committed
	local module = e.module
	if not data then print("[ONLINE PROFILE] get configs") return end
	for i = 1, #data do
		local val = data[i]
dg's avatar
dg committed

		if module == "generic" then
			self:saveGenericProfile(e.kind, val, true, i < #data)
dg's avatar
dg committed
		else
			self:saveModuleProfile(e.kind, val, true, i < #data)
dg's avatar
dg committed
		end
	end
dg's avatar
dg committed
	if self.evt_cbs.GetConfigs then self.evt_cbs.GetConfigs(e) self.evt_cbs.GetConfigs = nil end
end

dg's avatar
dg committed
function _M:eventPushCode(e)
dg's avatar
dg committed
	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
dg's avatar
dg committed
	end
dg's avatar
dg committed
end

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

dg's avatar
dg committed
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)
dg's avatar
dg committed
	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

-----------------------------------------------------------------------
dg's avatar
dg committed
-- Orders for the profile thread
-----------------------------------------------------------------------

DarkGod's avatar
DarkGod committed
function _M:getNews(callback, steam)
	print("[ONLINE PROFILE] get news")
	self.evt_cbs.GetNews = callback
DarkGod's avatar
DarkGod committed
	if not steam then core.profile.pushOrder("o='GetNews'")
	else core.profile.pushOrder("o='GetNews' steam=true")
	end
	print("[ONLINE PROFILE] auth")
DarkGod's avatar
DarkGod committed
	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
dg's avatar
dg committed
function _M:logOut()
	core.profile.pushOrder(table.serialize{o="Logoff"})
	profile.generic.online = nil
	profile.auth = nil
dg's avatar
dg committed
	local pop = self:mountProfile(true)
	fs.delete("/generic/online.profile")
DarkGod's avatar
DarkGod committed
	fs.delete("/generic/onlinesteam.profile")
dg's avatar
dg committed
	self:umountProfile(true, pop)
dg's avatar
dg committed
end

function _M:getConfigs(module, cb, mod_def)
dg's avatar
dg committed
	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
dg's avatar
dg committed
		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
function _M:setConfigsBatch(v)
	core.profile.pushOrder(table.serialize{o="SetConfigsBatch", v=v and true or false})
end

function _M:setConfigs(module, name, data)
	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))})
function _M:syncOnline(module, mod_def)
	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
dg's avatar
dg committed
		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)
dg's avatar
dg committed
end

function _M:checkModuleHash(module, md5)
	self.hash_valid = false
dg's avatar
dg committed
	if not self.auth then return nil, "no online profile active" end
dg's avatar
dg committed
	if config.settings.cheat then return nil, "cheat mode active" end
	if game and game:isTainted() then return nil, "savefile tainted" end
dg's avatar
dg committed
	core.profile.pushOrder(table.serialize{o="CheckModuleHash", module=module, md5=md5})

	self:waitEvent("CheckModuleHash", function(e) ok = e.ok end, 10000)
dg's avatar
dg committed

	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

dg's avatar
dg committed
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})
function _M:registerNewCharacter(module)
	if not self.auth or not self.hash_valid then return end
dg's avatar
dg committed
	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)
dg's avatar
dg committed
	dialog:done()
	if not uuid then return end
	print("[ONLINE PROFILE] new character UUID ", uuid)
	return uuid
end

dg's avatar
dg committed
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

dg's avatar
dg committed
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)
dg's avatar
dg committed
	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
dg's avatar
dg committed
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
dg's avatar
dg committed
	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

dg's avatar
dg committed
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)
dg's avatar
dg committed
	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
dg's avatar
dg committed
end

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

function _M:newProfile(Login, Name, Password, Email)
	print("[ONLINE PROFILE] profile options ", Login, Email, Name)
dg's avatar
dg committed
	core.profile.pushOrder(table.serialize{o="NewProfile2", login=Login, email=Email, name=Name, pass=Password})
	local id = nil
DarkGod's avatar
DarkGod committed
	local reason = nil
	self:waitEvent("NewProfile2", function(e) id = e.uid reason = e.reason end)
DarkGod's avatar
DarkGod committed
	if not id then print("[ONLINE PROFILE] could not create") return nil, reason or "unknown" end
dg's avatar
dg committed
	print("[ONLINE PROFILE] profile id ", id)
dg's avatar
dg committed
	self:performlogin(Login, Password)
DarkGod's avatar
DarkGod committed
	return id
dg's avatar
dg committed
end
dg's avatar
dg committed

dg's avatar
dg committed
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

dg's avatar
dg committed
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:addonEnableUpload()
	if not self.auth then return end
	core.profile.pushOrder(table.serialize{o="AddonEnableUpload"})
	print("[ONLINE PROFILE] enabling addon upload grants")
end

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

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