00003 不思议迷宫.0009.4:攻防计算
据说GG大玩家上有攻击和闪避的mod,有时能用,有时会出错。抱着研究的目的,我就试了试,确实如此。我先修改将伤害提高到10000倍,就报数据异常。后来将伤害增加300,重新进本,一直到30多层都没有问题。然后我暂离,再进,发现我的角色已经挂了。这说明服务器端的一些计算并没有错。后来继续用这个+300伤害,过了惑星人3层本(Boss只有800血,两刀砍死),居然拿到了奖励。退出重进后,发现奖励还在。——我能感叹下,这个游戏这是太神奇了嘛。
如果官方看到了我的这篇文章,请立即修正这个bug。
欢迎大家进群(161355323)讨论游戏破解和防破解方面的话题,为提高游戏数据的安全性作出贡献。
――――――――――――――――――――――――――――――
当玩家点击一个怪物后,会发生哪些事情呢?当然是攻击怪物,然后怪物反击,然后检测是否死亡……攻击处理在哪里?
对于地牢中的格子,一切都在UIGrid.luac中。查看它的构造,很容易就找到这么一句:
-- 攻击怪物(或boss)的点击操作
self.attackMonsterClick = nil;
查找attackMonsterClick:
-- 创建怪物
function UIGrid:createMonster()
……
localfunction onClicked(sender, eventType)
……
ifeventType == ccui.TouchEventType.ended then
……
-- 怪物攻击先在此处模拟
DungeonActionM.go("physic_attack", self.gridData:getPos());
EventMgr.fire(event.PLAYER_MOVE, self.index);
return true;
end
end
……
self.attackMonsterClick = onClicked;
end
看看注释,“怪物攻击先在此处模拟”,实在是不知道说什么好了,这完全就是误导。在第一眼的时候,我想到的是,“角色攻击在别处计算”。然而,事实上并不是这样。
打开DungeonActionM.go:
-- 客户端执行一条action指令
-- TODO: 待所有指令都调整完毕后,需要把客户端验证的流程也整合进来
function go(cmd, pos, data, extra)
local record= {["cmd"] = cmd, ["pos"] = pos, ["data"] = data,["extra"] = extra, };
DungeonLogM.addRecord(record);
DungeonDebugM.addAction({["cmd"] = cmd, ["pos"] =pos, ["data"] = data, ["extra"] = extra, });
local mod =rules[cmd];
Profiler.funcBegin("action:" .. cmd);
local ret,added = mod.doAction({ ["pos"] = pos, ["data"] = data },extra);
if ret ~=false and true ~= added then
-- 部分指令已经自行添加了action,就不重复添加
-- 执行成功了,添加到同步队列中
DungeonM.addAction({ ["cmd"] = cmd, ["data"] = data,["pos"] = pos, });
end
if ret ~=false then
-- 标记一下是有效的action
record["done"] = 1;
end
-- 如果需要即时保存一下
if notisVerifyClient() and needSave then
needSave= false;
go("save_dungeon");
end
Profiler.funcEnd("action:" .. cmd);
return ret;
end
虽然我写代码的水平也很烂,没有实力没有立场,但还是先容我先吐槽一下这段代码。
local record= {["cmd"] = cmd, ["pos"] = pos, ["data"] = data,["extra"] = extra, };
DungeonLogM.addRecord(record);①
DungeonDebugM.addAction({["cmd"] = cmd, ["pos"] =pos, ["data"] = data, ["extra"] = extra, });②
又是Log又是Debug的,也许真的需要这么分级?②处为啥不是DungeonDebugM.addAction(record);?
local mod =rules[cmd];
rules是rule的复数,看起来是个集合,对其的下标访问返回的应当是个元素。保存的变量名为啥是mod而不是单数形式的rule?mod和rules之间,必然有一个是命名错误的。
local ret, added = mod.doAction({ ["pos"] = pos,["data"] = data }, extra);
ret是什么意思?我猜测是表示doAction这一动作是否成功,但也可能表示地球是否停转?
if ret ~= false and true ~= added then
在某些语言中,为了防止将比较误写为赋值,有人建议将常量写在前头。比如added == true可能被误写为added = true,而编译器/解释器并不会报错,就当成赋值处理。但是如果反过来写,true = added,这就错了,常量不能被赋值。这个方法不能说错,但显然违反了正常人的阅读和判断习惯。一个较好的办法是使用isSame、isEqual、isTrue、isFalse之类的函数。如果函数很复杂,就配上单元测试。
if ret ~=false and true ~= added then
-- 部分指令已经自行添加了action,就不重复添加
-- 执行成功了,添加到同步队列中
DungeonM.addAction({ ["cmd"] = cmd, ["data"] = data,["pos"] = pos, });
end
前面说过,良好的程序代码都应当是自解释的。再看看这个,“ret ~= false and true ~=added”这个所表达的意思,一眼是看不明白的,要细思慢想,或者,看下面的注释。将判断提取为短小的具名函数可好?
function actionHasBeenDoneSuccessfullyAndNotAdded (……)
名字有点长,这都让人不喜。中间还有个And,说明它作了两件事,还不够简单,也让人不喜。那分开?
function actionHasBeenDoneSuccessfully(……)
function actionHasBeenAdded (……)
ifactionHasBeenDoneSuccessfully(action) and not actionHasBeenAdded(action) then
end
原则上没有错,但在本处,显得……呃……
其实,作两件事的锅应该由mod.doAction来背。从名称上来看,这个函数只干一件事,但偏偏返回了两个值,可以考虑将它们分开:mod.doAction、DungeonM.actionHasBeenAdded。也许,将action抽取出来,定义为一个显式类型/概念,会更好:action.do、action.hasBeenAdded。
if ret ~=false then
-- 标记一下是有效的action
record["done"] = 1;
end
record是一个局部变量,仅在DungeonLogM.addRecord(record);中被使用。在此处来这么一句,会有啥影响吗?如果DungeonLogM.addRecord中保存的是record的引用,那就有;如果保存的是record的拷贝,那就没有。这可能从一定程度上解释了DungeonDebugM.addAction……) 的参数为啥不是record了。即便如此,我们也有更好的办法:
DungeonDebugM.addAction(clone(record));
record["done"] = 1到底有什么用?跟踪到DungeonLogM中——里面又有一大堆可以说道说道的内容,还是先略过吧,直接看用到done的地方:
function stepPlay()
……
local record= ME.user.actionRecord or {};
local index= ME.user.replayIndex or 1;
……
local action= record[index];
ifaction.done then
localprogress = ME.user.actionProgress;
ME.user.actionProgress = progress + 1;
print("******************* 回放进度 " ..progress + 1 .. "/" .. ME.user.actionSum .. "*******************");
……
end
……
end
不要被函数中的局部变量的名称所迷惑;此record并非彼record,action才是。现在,似乎明了了:被标记为done的,将被回放。是这样吗?这个我就不深究了。——我只想说,done确实有用,那么在DungeonActionM.go中就应当让它突出一点,不要像现在那样看起来没什么用处。如果notdone的record在DungeonLogM中没有用处,那么就应当在doAction成功后调用DungeonLogM.addRecord(record)。如果有用,不如弄个函数明确一下,至少也可以让“done”这个内部变量/字符串不要随便蔓延:
DungeonLogM.setRecordStatus(record, 1);
终于来到了最后一段:
-- 如果需要即时保存一下
if notisVerifyClient() and needSave then
needSave= false;
go("save_dungeon");
end
莫名其妙的needSave。go(cmd)中为毛要save_dungeon?
needSave在下面的这个函数中被赋值:
function immediatelySave()
needSave =true;
end
为毛不在这个函数中执行save_dungeon?如果是它是一个延时调用,为毛起名“immediatelySave”?改成saveWhenNextGo是不是好点?
吐槽就先到这里,继续说说DungeonActionM.go("physic_attack",self.gridData:getPos());具体执行了什么。go函数中最重要的一句是mod.doAction({["pos"] = pos, ["data"] = data }, extra)。mod是rules[cmd],cmd是"physic_attack"。rules是什么鬼?排查后发现:
function init()
if not _initthen
loadCsv();
-- 载入所有的规则处理子模块
rules =LOAD_PATH("game/logic/module/dungeon_actions");
end
end
rules就是目录game/logic/module/dungeon_actions下的全部luac文件的返回值,我们关心的"physic_attack":
return {
doAction =function(action)
-- 怪物攻击先在此处模拟
localpos = action.pos;
localgrid = DungeonM.getGridByPos(pos);
returnSkillM.physic(ME.user, grid.monster);
end,
};
又跑到了SkillM.physic中:
-- 物理攻击
function physic(source, target, noSync)
……
-- 1. 怪物攻击
-- 怪物攻击起始动作
……
-- 2. 玩家攻击
-- 玩家攻击起始动作
initSequence(source, target, 0);
sequenceList[source]:start(source, target, 0);
if notFormulaM.invoke("HAPPEN_DODGE", source, target,DungeonM.getRandSeed("HAPPEN_DODGE")) then
-- TODO:闪避不及,需要计算具体的伤害
Profiler.funcBegin("physic1-2");
hit(source, target, PHYSIC_ATTACK, { ["countered"] =countered, });
Profiler.funcEnd("physic1-2");
-- 触发aoe
……
-- 触发溅射同排怪
……
else
-- 闪避掉了
……
end
……
end
看看,看看,1. 怪物攻击、2. 玩家攻击,这和开始时的注释“怪物攻击先在此处模拟”不同啊。玩家攻击中,值得关心的有两点,一个是FormulaM.invoke("HAPPEN_DODGE"),另一个是hit。
FormulaM.invoke("HAPPEN_DODGE")的作用是调用src/game/farmula目录下的HAPPEN_DODGE.luac文件的返回值:
-- 计算是否发生闪避
return function(source, target, rand)
-- 如果有必中属性
local prop =PropM.combine(source, "true_strike", 1);
ifPropM.apply(prop, 1) > 0 then
returnfalse;
end
-- 如果有必闪属性
prop =PropM.combine(target, "true_dodge", 1);
ifsource:queryAttrib("attack") < PropM.apply(prop, 1) then
returntrue;
end
-- TODO: 需要source命中 - target闪避来计算概率
localaccuracy = source:getAccuracy();
local dodge= target:getDodge();
-- 召唤兽忽视敌人闪避
ifsource.type == OBJECT_TYPE_SUMMON then
-- 忽视闪避属性
prop = PropM.combine(source,"summon_ignore_dodge", 1);
dodge =PropM.apply(prop, dodge);
end
localhitRatio = accuracy - dodge; -- 命中的概率
rand = rand% 100;
……
-- 是否闪避(不命中)
return rand>= hitRatio;
end
可以看出,必中的优先级高于必闪。
在函数的开头,我们可以做个简单的判断:
if target.type== OBJECT_TYPE_MONSTER then
returnfalse;
elseif target.type== OBJECT_TYPE_USER then
returntrue;
end
如果目标是怪,必然不闪避;如果目标是主角,必然闪避;其他的,比如召唤兽,正常计算。
hit用于伤害计算:
-- 对目标物理打击扣血
function hit(source, target, skillId, extra_data)
……
-- 玩家扣血、触发
local attack= source:getAttack();
……
-- 伤害为攻击的倍数(暂时只能为召唤兽)
local prop =combine(source, "multiple_damage", 1);
if prop[3]> 0 then
attack =trigger(source, prop[1], prop[2], attack);
end
-- 被敌方百分比削弱
prop =combine(target, "weak_enemy", "attack");
attack =PropM_apply(prop, attack);
-- 绝对值削弱
prop =combine(target, "weak_enemy2", "attack");
attack =PropM_apply(prop, attack);
-- 基础伤害等于攻击
local damage= attack;
……
----------------- 计算source的prop影响下的总伤害-------------------------------
----------------- 先计算总的加成,最后再相加(百分比计算时不叠加)---------------
-- 概率额外伤害
……
-- 古剑术(概率额外伤害)
……
-- 醉拳(概率额外伤害)
……
----------------- 计算target的prop影响下的总伤害 ----------------------
----------------- 先百分比后绝对值,百分比计算时直接叠加 ---------------
……
-- 物理抗性
……
-- 忽视抗性
……
-- 开板降低物抗
……
-- 最终伤害,不能低于0
damage =damage + addon;
damage =math.max(damage, 0);
……
-- 概率触发技能
……
-- 亡灵契约:概率触发技能
……
-- 雷神之锤:概率触发技能
……
end
最终伤害,乘上1万倍好了。
――――――――――――――――――――――――――――――
以上说这么多都是没用的,客户端修改伤害并不会影响服务器端的计算。所谓的攻击和闪避mod,只是逗人玩的。