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

2024-04-08 09:48

本文主要是介绍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中。它会在其他什么地方同步到服务器吗?

这篇关于00003 不思议迷宫.0004:客户端数据缓存的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/885241

相关文章

大模型研发全揭秘:客服工单数据标注的完整攻略

在人工智能(AI)领域,数据标注是模型训练过程中至关重要的一步。无论你是新手还是有经验的从业者,掌握数据标注的技术细节和常见问题的解决方案都能为你的AI项目增添不少价值。在电信运营商的客服系统中,工单数据是客户问题和解决方案的重要记录。通过对这些工单数据进行有效标注,不仅能够帮助提升客服自动化系统的智能化水平,还能优化客户服务流程,提高客户满意度。本文将详细介绍如何在电信运营商客服工单的背景下进行

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

使用SecondaryNameNode恢复NameNode的数据

1)需求: NameNode进程挂了并且存储的数据也丢失了,如何恢复NameNode 此种方式恢复的数据可能存在小部分数据的丢失。 2)故障模拟 (1)kill -9 NameNode进程 [lytfly@hadoop102 current]$ kill -9 19886 (2)删除NameNode存储的数据(/opt/module/hadoop-3.1.4/data/tmp/dfs/na

异构存储(冷热数据分离)

异构存储主要解决不同的数据,存储在不同类型的硬盘中,达到最佳性能的问题。 异构存储Shell操作 (1)查看当前有哪些存储策略可以用 [lytfly@hadoop102 hadoop-3.1.4]$ hdfs storagepolicies -listPolicies (2)为指定路径(数据存储目录)设置指定的存储策略 hdfs storagepolicies -setStoragePo

Hadoop集群数据均衡之磁盘间数据均衡

生产环境,由于硬盘空间不足,往往需要增加一块硬盘。刚加载的硬盘没有数据时,可以执行磁盘数据均衡命令。(Hadoop3.x新特性) plan后面带的节点的名字必须是已经存在的,并且是需要均衡的节点。 如果节点不存在,会报如下错误: 如果节点只有一个硬盘的话,不会创建均衡计划: (1)生成均衡计划 hdfs diskbalancer -plan hadoop102 (2)执行均衡计划 hd

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

烟火目标检测数据集 7800张 烟火检测 带标注 voc yolo

一个包含7800张带标注图像的数据集,专门用于烟火目标检测,是一个非常有价值的资源,尤其对于那些致力于公共安全、事件管理和烟花表演监控等领域的人士而言。下面是对此数据集的一个详细介绍: 数据集名称:烟火目标检测数据集 数据集规模: 图片数量:7800张类别:主要包含烟火类目标,可能还包括其他相关类别,如烟火发射装置、背景等。格式:图像文件通常为JPEG或PNG格式;标注文件可能为X

缓存雪崩问题

缓存雪崩是缓存中大量key失效后当高并发到来时导致大量请求到数据库,瞬间耗尽数据库资源,导致数据库无法使用。 解决方案: 1、使用锁进行控制 2、对同一类型信息的key设置不同的过期时间 3、缓存预热 1. 什么是缓存雪崩 缓存雪崩是指在短时间内,大量缓存数据同时失效,导致所有请求直接涌向数据库,瞬间增加数据库的负载压力,可能导致数据库性能下降甚至崩溃。这种情况往往发生在缓存中大量 k

pandas数据过滤

Pandas 数据过滤方法 Pandas 提供了多种方法来过滤数据,可以根据不同的条件进行筛选。以下是一些常见的 Pandas 数据过滤方法,结合实例进行讲解,希望能帮你快速理解。 1. 基于条件筛选行 可以使用布尔索引来根据条件过滤行。 import pandas as pd# 创建示例数据data = {'Name': ['Alice', 'Bob', 'Charlie', 'Dav