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



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是否做了什么特别的事:

-- 检索数据

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

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

这里,需要说一下的是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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值