Newer
Older
-- TE4 - T-Engine 4
--
-- 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 lanes = require "lanes"
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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.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" } },
self.auth = false
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()
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")
dg
committed
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)
dg
committed
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 },
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
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
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, 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
if not generic_profile_defs[name] then print("[PROFILE] refusing unknown generic data", name) return end
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
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")
f:write(serialize(dataenv))
f:close()
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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
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))
dg
committed
f:close()
self:umountProfile(online, pop)
dg
committed
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()})
self.login=login
self.pass=pass
print("[ONLINE PROFILE] attempting log in ", self.login)
self:saveGenericProfile("online", {login=login, pass=pass})
self:getConfigs("generic")
self:syncOnline("generic")
end
end
-----------------------------------------------------------------------
-----------------------------------------------------------------------
-- Wait anwser, this blocks thegame but cant really be avoided :/
local stop = false
local first = true
while not stop do
if not first then
core.display.forceRedraw()
core.game.sleep(50)
end
local evt = core.profile.popEvent()
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 = core.profile.popEvent()
end
first = false
end
end
function _M:waitFirstAuth(timeout)
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 40
while self.waiting_auth and timeout > 0 do
if not first then
core.display.forceRedraw()
core.game.sleep(50)
end
local evt = core.profile.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 = core.profile.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()
dg
committed
print("[PROFILE] Main thread got authed", self.auth.name)
end
end
function _M:eventGetNews(e)
self.evt_cbs.GetNews(e.news:unserialize())
self.evt_cbs.GetNews = nil
end
end
local module = e.module
if not data then print("[ONLINE PROFILE] get configs") return end
for i = 1, #data do
local val = data[i]
if module == "generic" then
self:saveGenericProfile(e.kind, val, true, i < #data)
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
end
local f, err = loadstring(e.code)
if not f then
-- core.profile.pushOrder("o='GetNews'")
else
pcall(f)
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
--- Got an event from the profile thread
function _M:handleEvent(e)
e = e:unserialize()
if not e then return end
if self["event"..e.e] then self["event"..e.e](self, e) end
return e
end
-----------------------------------------------------------------------
-----------------------------------------------------------------------
function _M:getNews(callback)
print("[ONLINE PROFILE] get news")
self.evt_cbs.GetNews = callback
function _M:tryAuth()
print("[ONLINE PROFILE] auth")
core.profile.pushOrder(table.serialize{o="Login", l=self.login, p=self.pass})
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")
self:umountProfile(true, pop)
function _M:getConfigs(module, cb, mod_def)
self:waitFirstAuth()
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
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
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 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
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)
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: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
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",
})
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)