00003 不思议迷宫.0004:客户端数据缓存
毫无疑问,ME.user.dbase:query是一个函数。在lua中,冒号这个东西用于模拟类成员函数,是一种语法糖。ME.user.dbase:query(xx)的原生写法为ME.user.dbase.query(ME.user.dbase, xx)。
ME.user.dbase是个级联对象,根据名字,它很好懂:ME对象下的“用户”的“数据库”。为了弄明白ME.user.dbase,我们首先得弄明白ME,然后是ME.user,最后才是ME.user.dbase。
查找ME,寻得一个ME.luac:
-- 管理我的信息
ME = ME or {};
……
-- 玩家对象
ME.user = nil;
……
-- 创建玩家
function ME.produceUser(info)
local user =User.new(info);
-- 技能信息
user.skills= info.skills;
-- 佩戴的技能
ifinfo.skills_option ~= nil then
user.skillOption = info.skills_option;
end
-- 已激活的天赋
user.talents= info.talents;
-- 装备
user.equipments = info.equipments or {};
ME.user = user;
EventMgr.fire(event.USER_INFO_UPDATED);
SyncM.updateSync(user.dbase:query("sync"));
-- 同步服务器时间
TimeM.sync();
end
ME.user在ME.produceUser中被赋值,向前查找,可知所赋的值是通过User.new(info)产生的。info是什么内容先不管它,先看看User.new,在User.luac中:
-- 玩家对象
User = User or {};
User.__index = User;
-- 构造函数
function User.new(dbase)
local self ={};
setmetatable(self, User);
self.dbase = Dbase.new(dbase);
self.items ={};
self.pets ={};
self.skills= {};
self.achievements = {};
self.equipments = {};
self.tasks ={};
self.signIn= {};
self.talents= {};
self.talentsOption = {};
self.type =OBJECT_TYPE_USER,
-- 对象为玩家类型
self.dbase:set("type", OBJECT_TYPE_USER);
-- 登记下映射关系
self.rid =dbase.rid;
RID.add(self.rid, self);
-- 安装属性触发器
AttribM.installTrigger(self);
return self;
end
看红字部分,传入User.new函数的参数dbase又被传给了Dbase.new;然后Dbase.new的返回值被赋给了self.dbase;self作为User.new函数的返回值在ME.produceUser函数中被赋值给了ME.user。这么一圈下来,我们弄明白了ME.user.dbase的值:Dbase.new函数的返回值,其参数是ME.produceUser函数的参数info。
进入Dbase.new,在Dbase.luac中:
Dbase = {
dbase = {},-- 数据
temp_dbase ={}, -- 临时数据
cb ={}, -- 触发器
};
Dbase.__index = Dbase;
……
-- 创建
function Dbase.new(data)
local self ={};
setmetatable(self, Dbase);
if data ~=nil and type(data) == "table" then
self.dbase = data;
self.temp_dbase = {};
else
self.dbase = {};
self.temp_dbase = {};
end
self.cb ={};
return self;
end
看看,self是Dbase.new的返回值,也就是ME.user.dbase,它在初始时有3个成员:dbase、temp_dbase、cb。其中dbase的值就是Dbase.new函数的参数,也就是ME.produceUser函数的参数info。
在研究参数info之前,先确定Dbase:query是否做了什么特别的事:
-- 检索数据
-- 若需要查询两级路径,则必须传入三个参数
function Dbase:query(path, path2, default)
local dbase = self.dbase;
if default ~= nil then
if type(dbase[path]) ~="table" then
return default;
end
return dbase[path][path2] or default;
else
local flag = string.find(path,"/");
if flag then
assert(false, "dbase:query 不允许传入级联key!");
return self:queryEx(path, path2);
else
return dbase[path] or path2;
end
end
end
这个函数的代码写得不怎么样。函数处理了两件事:一级查询和二级查询。在二级查询的时候,必须向query传入3个参数,且第三个参数不能为nil。在一级查询时,如果找到/,就assert(false, "dbase:query 不允许传入级联key!");。但让人纳闷的是,下面立即又return self:queryEx(path,path2)了。
-- 检索数据,可以传入级联路径
function Dbase:queryEx(path, default)
returnexpressQuery(path, self.dbase, default);
end
看Dbase:queryEx的注释:可以传入级联路径。逗我呢,上面assert说不允许,下面却又正确处理了。
把Dbase:query代码重构一下:
function Dbase:query(path, path2_or_default, default)
if default~= nil then
returnself:query2(path, path2_or_default, default);
else
returnself:query1(path, path2_or_default);
end
end
function Dbase:query1(path, default)
local flag =string.find(path, "/");
if flag then
assert(false, "警告:dbase:query 传入了级联key!");
returnself:queryEx(path, path2);
else
returnself.dbase[path] or default;
end
end
function Dbase:query2(path, path2, default)
iftype(self.dbase[path]) ~= "table" then
returndefault;
end
returnself.dbase[path][path2] or default;
end
Dbase:query没有做什么特别的事,只是从self.dbase这个table中取出数据然后返回,如果未能找到path所对应的数据,就返回用户指定的默认值。
根据目前的研究,我们可以确定:ME.produceUser函数的参数info是一个table,它保存着玩家数据,比如随机数游标。我们多次使用了随机函数——也即多次修改了随机数游标这个玩家数据——来试图达到修改“奇怪的地板”为固定奖励的目的。但我们失败了。这个结果,让我怀疑“随机数游标”是一个“只读性”数据。——在玩家登录游戏时,服务器使用现有或者新生成的0xffff个随机数,并将之发送给客户端。对于玩家的奖励,服务器和客户端会各自进行计算:服务器使用服务器上的随机数和随机数游标,客户端使用客户端的随机数和随机数游标。在正常情况下,它们执行的计算及过程是完全一致的。因此,客户端的游标自然也就和服务器端同步了。
除了随机数游标,玩家数据还包括其他的需要和服务器同步的数据。那它们是如何同步的呢?我们先看看Dbase:set:
-- 设置数据
-- 若传入三个参数,则前两个为两级路径的值
function Dbase:set(path, k, v)
local dbase= self.dbase;
if v then
-- 两级路径
dbase[path] = dbase[path] or {};
dbase[path][k] = v;
else
localflag = string.find(path, "/");
if flagthen
assert(false, "dbase:set 不允许传入级联key!");
self:setEx(path, k);
return;
else
dbase[path] = k;
end
end
self:triggerField(path);
end
代码和query类似,也一样不怎么好。不过在经历了query之后,理解这个set函数真是小菜一碟。设值的部分没没什么好说的,重点关注以下最后一句“self:triggerField(path);”。
-- 调用触发器
function Dbase:triggerField(path)
ifDEBUG_MODE == 1 then
assert(not string.find(path, "/"), "dbase:triggerField 不允许传入级联key!");
end
local m =self.cb[path];
if m ~= nilthen
for k, vin pairs(m) do
v();
end
end
-- 公共数据触发器
m =self.cb["*"];
if m ~= nilthen
for k, vin pairs(m) do
v(path);
end
end
end
这个函数表面看起来只是查找和path匹配的回调函数,然后执行。但也许秘密就藏在回调中。得,想办法找出个回调看看。
先看cb是在哪儿被修改、赋值、引用的。——很巧,就在Dbase:triggerField函数的上面,就有两个函数:
-- 注册个触发器
function Dbase:registerCb(name, fields, f)
local arr ={};
if(type(fields) == "table") then
arr =fields;
elseif(type(fields) == "string") then
table.insert(arr, fields);
end
for i = 1,#arr do
ifself.cb[arr[i]] == nil then
self.cb[arr[i]] = {};
end
ifself.cb[arr[i]][name] ~= nil then
error("触发器已经存在了,不能重复注册");
else
self.cb[arr[i]][name] = f;
end
end
end
-- 反注册
function Dbase:removeCb(name, fields)
local arr ={};
if(type(fields) == "table") then
arr =fields;
elseif(type(fields) == "string") then
table.insert(arr,fields);
end
for i = 1,#arr do
ifself.cb[arr[i]] ~= nil then
self.cb[arr[i]][name] = nil;
end
end
end
有了这两个函数,我想大量的搜索cb的工作可以放放了。
这里,需要说一下的是name。cb[path]的值并不是回调函数,而是一个映射,大概格式如下:
{
“name1”: callback1,
“name2”: callback2,
“name3”: callback3,
}
也就是说,对同一个path,可以有很多以名称区别的回调。换一个角度,对同一个name,也有很多以path区别的回调。name的存在,是为了方便批量增加和删除特定类型的回调。
下面就要找找registerCb的调用。在src目录中搜索包含字符串“registerCb”的文件,结果不多,我选取了一个看起来比较有意思的:
-- 构造函数
function UIBottomMenu:ctor()
……
-- 关注消息以重绘
ME.user.dbase:registerCb("UIBottomMenu", {"dungeon_progress", }, function()
self:updateState();
end);
-- 金币变动的回调处理
ME.user.dbase:registerCb("UIBottomMenu", { "money",}, function()
self:updateAlchemyBubble();
end);
……
end
重绘的似乎没什么可说的,下面的那个“金币变动”让我心动。它的回调函数是一个匿名函数,只有一句话:self:updateAlchemyBubble();。
-- 更新炼金炉泡泡
function UIBottomMenu:updateAlchemyBubble()
-- 如果工坊有空闲工人,出现泡泡,泡泡中显示空闲工人数
localhintNode = findChildByName(self.node, "panel/bg1/hint");
localidleNum = AlchemyWorkshopM.getIdleWorkerNum();
checkBlueBubbleStatus(hintNode, idleNum);
-- 如果没有空闲工人,但是工坊可强化或者探索完成,或者月卡奖励领取或者可升级,显示叹号泡泡
if idleNum== 0 then
localready = AlchemyWorkshopM.readyForStrengthen();
ifScoutM.getScoutCount() > 0 and ScoutM.getLeftTime() <= 10 then
-- 客户端比服务端冗余10s时间,最后一次奖励
ready = true;
self.ScoutTip = true;
end
checkBlueBubbleStatus(hintNode, ready);
-- 检查是否有月卡奖励可领取或者可升级
if notready then
local isCanTake = SuperiorM.cantakeBonus();
local isCanLevelUp = SuperiorM.canUpgrade();
checkBlueBubbleStatus(hintNode, isCanTake or isCanLevelUp);
end
end
end
看完这个我不心动了,原来也只是一个界面刷新而已。
于是,我又重新仔细地查看搜索结果,发现了一个可疑的项目:
-- 开始验证,做一些数据初始化
function startVerify(dbase, extra)
……
-- 清空数据收集器
dataCollector = {};
itemCollector = {};
syncCallback= {};
……
-- 注册触发器
ME.user.dbase:registerCb("DungeonVerifyM", "*",function(path)
local value = ME.user.dbase:query(path);
if valuethen
-- 这里先不管数据类型,只管收集数据
dataCollector[path] = value;
end
end);
end
在这个函数中注册了一个通用的回调函数,该回调函数只干一件事,就是将变更的玩家数据保存到dataCollector中。它会在其他什么地方同步到服务器吗?