搜索dataCollector,发现全部都在DungeonVerifyM.luac中,这让事情简单不少。经过排查,我发现了这么一个函数:
-- 同步结果回服务器
function packResultData(mappingList)
local dbase= {};
-- 删掉没用的数据
local filter= { "attackerInfo", "hitRound", "attrib_coding","current_dungeon", };
for _,attrib in pairs(filter) do
dataCollector[attrib] = nil;
end
-- 先抛出一个事件,其他地方关注做一些动作
EventMgr.fire(event.START_PACK_VERIFY_RESULT);
-- 已经没有的数据
for k, _ inpairs(dataCollector) do
if notME.user.dbase:query(k) then
dataCollector[k] = nil;
end
end
-- temp下的也干掉,不然会造成死循环
dataCollector.temp = nil;
-- 道具统计需要另外处理
dataCollector.daily_item_stat = nil;
-- TODO: 装备
dataCollector.equipments = ME.user.equipments;
-- 把randomCursor移到temp下
local temp ={};
localrandomCursor = dataCollector.randomCursor or {};
dataCollector.randomCursor = nil;
temp["random_cursor"] = to_mapping(randomCursor);
--temp["dungeon_class_stat"] = DungeonStatM.queryTempStat();
-- 先把整数过滤出来,整理成差值
localdifferent = {};
local backUp= ME.user.dbaseBackUp or {}; -- 备份的数据
for k, v inpairs(dataCollector) do
localoriginal = backUp[k] or 0; -- 原始数据
-- 整数
iftype(v) == "number" and type(original) == "number" then
-- 以差值的形式
different[k] = v - original;
print("key = " .. k .. " v = " .. v .. " ".. original);
dataCollector[k] = nil;
else
-- 其他
dbase[k] = v;
end
end
-- 收取dbase
for k, v inpairs(collectDbase(backUp, dbase)) do
dbase[k]= v;
end
-- 专门解析一遍
packDbase(dbase, mappingList);
-- TODO: 会造成深度拷贝,这里需要优化
dbase =toMapping(dbase);
-- 对比原始数据和现在的数据,得到被删除的数据
localdropDatas = {};
for path, _in pairs(backUp) do
local v= ME.user.dbase:query(path);
if not vthen
-- 被删除掉了,记录下来
table.insert(dropDatas, path);
end
end
-- 2. 道具,也是用差值的形式
local items= { __mapping = 1, };
for classId,_ in pairs(itemCollector) do
localoriginal = ME.user.itemBackUp[classId] or 0;
items[classId] = ItemM.getAmount(ME.user, classId) - original;
end
-- TODO:buff_data收集
local cfg =BuffDataM.getBuffDataCfg();
localbuffData = { __mapping = 1 };
for attrib,_ in pairs(cfg) do
-- 有变动的buff_data才需要同步
localvalue = ME.user.dbase:query(attrib);
ifdbase[attrib] and value then
-- 从dbase中删除
dbase[attrib] = nil;
buffData[attrib] = {};
fork, v in pairs(value) do
if v > 0 then
buffData[attrib][k] = v;
end
end
end
end
-- 战斗统计数据
dbase.combat_stat = CombatStatM.packCombatStat();
dbase.combat_stat.__mapping = 1;
-- 这个统计是记载buffer中的,需要弄出来
dbase.dungeon_field_stat = nil;
dbase.buildingBonusStat = nil;
dbase.learned_career_data = nil;
-- 银行次数
dbase.bank_times = DungeonBankM.getExchangeTimes();
local pet =ME.user:getActivePet();
return {
["dbase"] =dbase,
["temp_dbase"] =temp,
["attrib_differ"] = different,
["item_differ"] =items,
["drop_attrib"] =dropDatas,
["buff_data"] =buffData,
["career"] =CareerM.packCareerData(pet:getCareerData()),
["achievements"] =AchievementM.packAchievements(ME.user.baseAchievements),
["game_achievement"] = GameAchievementM.packGameAchievement(ME.user.baseGameAchievement),
["dungeon_field_stat"] =to_mapping(ME.user.dbase:query("dungeon_field_stat", {})),
["building_bonus_stat"] =to_mapping(ME.user.dbase:query("buildingBonusStat", {})),
["dungeon_class_stat"] = DungeonStatM.queryLayerStat(),
["learned_career_data"] =to_mapping(ME.user.dbase:query("learned_career_data", {})),
["instance_stat"] =DungeonInstanceM.packInstanceStat(ME.user.baseInstanceStat),
["daily_item_stat"] =to_mapping(ME.user.dbase:query("daily_item_stat")),
["element_diary"] =ElementAltarM.packElementDiary(ME.user.baseElementDiary),
["engineering_manual"] =ME.user.dbase:query("engineering_manual");
["add_pets"] =PetM.packAddPets(ME.user.basePetList),
["callback"] =to_mapping(syncCallback),
};
end
这个函数有点长,作用就是整理玩家数据。函数的最后有一个可疑的赋值:["callback"] = to_mapping(syncCallback)。函数的注释说是“同步结果回服务器”,但它其实并没有做这个工作,还是得看看它的调用者(msg_verify_dungeon_action.luac):
-- 验证地牢
return function(lpc)
local cookie= lpc.cookie;
localrid = lpc.rid;
localdbase = lpc.dbase;
localextra = lpc.extra;
localactions = extra.actions;
-- 创建一个玩家
ME.produceUser(table.deepcopy(dbase));
ME.loginNotifyOk();
-- 先切换迷宫状态
DungeonM.switchState(DUNGEON_LEAVE);
-- 初始化一些数据
DungeonVerifyM.startVerify(dbase, extra);
-- 构造迷宫
DungeonM.notifyDungeonData(extra.dungeon, extra.dungeon_context);
localuserData = DungeonLogM.getUserData();
-- 迷宫行为
DungeonActionM.doActions(actions);
-- mapping类型的需要先记录下来,一些确定的需要预先写下来
localmappingList = {
["equipments"] = true,
};
for key, vin pairs(lpc.dbase) do
iftype(v) == "table" and v.array ~= true then
-- mapping类型的
mappingList[key] = true;
end
end
-- 同步结果
local result= DungeonVerifyM.packResultData(mappingList);
local v ={["cookie"] = cookie, ["rid"] = rid, ["result"] =result, ["log"] = userData, };
Communicate.send("CMD_VERIFY_ACTION_RESULT", v);
end
看到verify、Verify、VERIFY这些字样,我心中就冒出一股不祥的预感,这做的莫非是验证而非保存。如果是验证,那么就应当有个验证结果返回给客户端,如有必要,就提示“数据异常”。那么验证结果是否有个回调函数呢?函数packResultData最后的那个可疑的赋值:["callback"] = to_mapping(syncCallback)中出现了callback字样,这里面会有吗?
搜索后发现,syncCallback的使用全部在DungeonVerifyM.luac中:在函数init中被设置一些值;在函数startVerify中被清空;在packResultData中被使用。
对syncCallback设值(DungeonVerifyM.init):
function init()
EventMgr.removeAll("DungeonVerifyM");
-- 注册触发器
EventMgr.register("DungeonVerifyM", event.ITEM_COUNT_UPDATED,function(classId)
-- 保存数据
itemCollector[classId] =ItemM.getAmount(ME.user, classId);
end);
-- 注册触发器
EventMgr.register("DungeonVerifyM", event.START_NEXT_FLOOR,function()
-- 回调是进入下一层
syncCallback["type"] ="next_floor";
end);
-- 暂离回调
EventMgr.register("DungeonVerifyM", event.DUNGEON_PAUSED,function()
-- 暂离
syncCallback["type"] ="pause";
end);
-- 注册触发器
EventMgr.register("DungeonVerifyM", event.LEAVE_DUNGEON,function()
-- 回调是进入下一层
syncCallback["type"] ="leave_dungeon";
syncCallback["is_dead"] =iif(ME.user:isDead(), 1, 0);
end);
-- 注册触发器
EventMgr.register("DungeonVerifyM", event.BACK_FLOOR,function(para)
-- 回调是进入下一层
syncCallback["type"] ="back_floor";
syncCallback["back_floor"] =para.back_floor;
syncCallback["skill_id"] = para.skill_id;
end);
-- 注册战斗结算事件回调
EventMgr.register("DungeonVerifyM", event.DUNGEON_COMBAT_END,function(para)
syncCallback["score"] =para.sum;
end);
end
没有和Verify直接相关的,也许Verify回调在它处设置。
于是又查找和Verify相关的文件,发现有VerifyMain.luac、DungeonVerifyM.luac以及msg_verify_dungeon_action.luac等等。
在VerifyMain.luac中发现了一个叫做“verifyRun”的函数:
-- 循环主函数,由C++代码在验证线程中的while循环中调用
function verifyRun()
-- 如果没有连接就不用管了
if notSocket.isConnected() then
return;
end
localfunction run()
-- 过一段时间通知一下服务器
ifos.time() - lastHeartbeat > 1 then
--每5秒钟心跳一次
lastHeartbeat= os.time();
Communicate.send("CMD_VERIFY_CLIENT_HEARTBEAT",{
["data"]= {
["pid"]= clientPid,
["lua_mem"]= collectgarbage("count"),
},
});
end
-- 开始连接服务器
Communicate.update();
end
localstatus, msg = xpcall(run, __G__TRACKBACK__)
if notstatus then
print(msg);
end
end
看函数注释可知,游戏在C++中开启了一个独立的线程,不停地执行verifyRun,而verifyRun会通过xpcall执行局部函数run,run中又调用了Communicate.update():
-- 网络相关的定时处理
function update()
-- socket的处理
Socket.update();
-- 处理消息
MSG.update();
-- 心跳
HeartBeatM.update();
-- 定时器
ScheduleM.update();
end
Communicate.update()中有包括了好几个update,心跳和定时器可以不用理会。
先看Socket.update():
-- 定时处理
function update()
if not isConnected() then return end
-- 发送数据
……
-- 取得数据
Profiler.funcBegin("Socket:recv");
recv();
Profiler.funcEnd("Socket:recv");
end
它干了两件事:如果有未发送的数据就发送,然后接收数据。从这个地方可以看出(好吧,代码被我省略了,但也不难想象),在执行Communicate.send的时候,并不会立即将数据发送给服务器,而是保存到一个本地缓存中。这在下面的这个函数中得到了验证——Socket.send:
-- 发送数据
function send(buffer)
if notisConnected() then return false end
-- 插入到队列中
table.insert(toSendData, { buffer, 0 });
return true;
end
下面再看看MSG.update:
-- 不停的处理消息
function update()
whilemsgQ[1] ~= nil do
localcmdNo = msgQ[1].cmdNo;
localpara = msgQ[1].para;
localcmdName = idNameMap[cmdNo];
……
-- 先将消息摘除掉(防止消息处理中报错,就进入死循环了)
table.remove(msgQ, 1);
-- 开始处理消息
local f= cmdModules[cmdNo];
if f =="TBL" then
local r = LOAD_RUN("game/cmd/" .. string.lower(idNameMap[cmdNo]));
cmdModules[cmdNo] = r;
f =r;
end
if not fthen
trace("MSG", "消息处理器(%d)不存在", cmdNo);
else
Profiler.funcBegin("MSG:" .. idNameMap[cmdNo]);
f(para);
Profiler.funcEnd("MSG:" ..idNameMap[cmdNo]);
end
-- 消息到达
messageArrival(idNameMap[cmdNo], para)
end
……
end
除了messageArrival这个函数名稍有歧义,别的都好理解:看消息队列中是否有未处理的消息,如果有就处理并从消息队列中删除。具体处理消息的函数是什么呢?一种是直接设置的回调,另一种是src/game/cmd/下的对应脚本返回的回调:
local r = LOAD_RUN("game/cmd/" ..string.lower(idNameMap[cmdNo]));
消息队列的填充在何处执行的呢?在上面的Socket.update有个取得数据的recv函数,在它里面有个Communicate.read调用,在Communicate.read中读取到数据之后又调用了processMsg:
-- 解析并分发处理一条消息
processMsg = function(cmdNo, body)
-- 如果是心跳信息,直接处理掉
if cmdNo ==0xff01 then
sendRaw(0xff02, body);
return;
end
-- 解包
local v = PktAnalyser.unpack(cmdNo,body);
if not vthen
cclog("解包失败了。");
returnfalse;
else
-- 仍到队列中
MSG.addMsg(cmdNo, v);
returntrue;
end
end
如果不是心跳包,并解包成功,就MSG.addMsg了:
-- 添加一条消息
function addMsg(cmdNo, para)
--cclog("addMsg cmdNo : %o, para :%o\n", cmdNo, para);
-- 如果是心跳信息,直接处理掉
if cmdNo ==0xff01 then
Communicate.sendRaw(0xff02, para.body);
return;
end
table.insert(msgQ, { cmdNo = cmdNo, para = para });
end
这里,将消息直接添加到了消息队列中。
回过头再说说messageArrival。让人不大理解的是,在取得消息并且处理完之后,按理说就应当没事了,为什么要再来一句messageArrival?
-- 消息到达
function messageArrival(cmd, para)
-- 这条消息处理完毕了
EventMgr.fire(event.MESSAGE_DONE, { cmd, para });
local cbList= msgTrigger[cmd] or {};
for _, f inpairs(cbList) do
iftype(f) == "function" then
f(para);
end
end
local m =waitMsgList[cmd]
if not mthen
return
end
if m.wait_time == -1 or
m.wait_time >= os.time() then
m.call_back(para)
end
waitMsgList[cmd] = nil
end
-- 注册等待消息
function waitMessageArrival(cmdNo, f, waitTime)
waitTime =waitTime or -1
waitMsgList[cmdNo] = {["call_back"] = f,["wait_time"] = waitTime + os.time(), }
end
上面给出了messageArrival和waitMessageArrival这两个函数的代码。看完它们后大概可以理解了,原来是部分消息在正常处理之后还可能需要另外一些特别的或者额外的处理,比如说将异步操作同步化(通过回调的方式)。messageArrival的名称起得不好,个人认为onNormallyProcessed、onMsgNormallyProcessed或许会更好点。
到此,验证相关的整体数据流程大概都清楚了,但是,CMD_VERIFY_ACTION_RESULT到底有没有回调?如果说没有,我才不信。那它在哪里?说真的,我确实没有从正面找到,只是在VerifyMain.luac中发现了下面这两个函数:
-- 接收到验证消息
-- 消息为lua的table字符串
-- 格式 { "cmd": 命令名, "body": 消息体 }
function onVerifyMsg(msg)
--print("验证线程收到消息:" .. msg);
localstatus, msg = xpcall(function() handleMsg(msg); end, __G__TRACKBACK__)
if notstatus then
print(msg);
--error(msg)
end
end
-- 处理消息
function handleMsg(msg)
-- 解析lua表
msg ="return " .. msg;
local t =loadstring(msg)();
local cmd =t.cmd;
local body =t.body;
--print("消息结构, cmd:" .. (cmd or"nil") .. ", body:" .. (tostring(body) or"nil"));
-- 交给逻辑验证模块
DungeonVerifyM.handleMsg(cmd, body);
end
我没能找到onVerifyMsg的引用点,哪怕是在cocos2dlua.so中(也许是我下载的安卓版和用于研究的iOS 2.7版有所不同,或者ida的bug)。
handleMsg最终最终委托给了DungeonVerifyM.handleMsg:
function handleMsg(cmd, body)
if cmd =="debug" then
local f= loadstring(body);
if fthen
f();
end
else
print("未识别的消息类型:" .. cmd);
end
end
虽然有些问题未能得到正面的证明,不过我相信msg_verify_dungeon_action.luac所干的就是不祥预感中的“验证”而非“保存”。
那么,现在问题来了,真正的“保存”到底在哪里?