00003 不思议迷宫.0004:客户端数据缓存

来源:互联网 发布:淘宝测款是什么意思 编辑:程序博客网 时间:2024/04/28 04:10


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.userME.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.dbaseself作为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

看看,selfDbase.new的返回值,也就是ME.user.dbase,它在初始时有3个成员:dbasetemp_dbasecb。其中dbase的值就是Dbase.new函数的参数,也就是ME.produceUser函数的参数info

在研究参数info之前,先确定Dbase:query是否做了什么特别的事:

--检索数据

--若需要查询两级路径,则必须传入三个参数

functionDbase: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的工作可以放放了。

这里,需要说一下的是namecb[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中。它会在其他什么地方同步到服务器吗?

0 0