Minecraft:退出重进大法的原理,到底是无敌时间还是重置高度
在玩MC的时候,总是会出现这样尴尬的局面:脚滑了,掉入万丈深渊。除了听天由命外,另一种选择就是快到地面时退出重进。对此现象的解释,玩家分成了两派:无敌时间派和重置高度派。
下面,我们将从代码层和用户层进行分析。
1.代码层原理
警告:此处会涉及到大量的代码逻辑,没有学习过编程的人可以直接查看用户层验证。
Minecraft源码来自由官方混淆表对1.14.4.jar进行反混淆反编译形成的源码,官方混淆表地址能在1.14.4+的版本JSON文件中找到,名为client_mappings。使用官方混淆表以保证代码纯净。
首先,我们了解一下玩家受到伤害的代码,这也是我们研究这个问题的切入点:
玩家收到伤害是由hurt方法进行处理,定义在net.minecraft.world.entity.Entity内,返回true为伤害成立,返回false则伤害取消。接着它被LivingEntity覆盖(没有调用super.hurt),之后被Player类覆盖。


Player的子类有两个:ServerPlayer和AbstractCilentPlayer。我们需要的hurt代码在ServerPlayer中,因为另一个类是进行客户端渲染占位的。

在这里我们看到了spawnInvulnerableTime,翻译为"出生无敌时间"。注意其他的条件,总结出来一共有五个条件:
服务器不能为专用服务器
PVP不被允许
伤害不能为掉落伤害
在出生无敌时间内
伤害不能为虚空伤害
其中,1,2,3点满足一点即可,4,5点必须满足
所以我们可以证明无敌时间确实能阻止摔落伤害发生:单机条件下,服务器为IntegratedServer,它不是"专用服务器",满足第一点。退出重进后的伤害一瞬间,如果在无敌时间内,并且不是掉到虚空外或kill带来的伤害(kill本质是数值极大,也就是Float.MAX_VALUE的虚空伤害),那么这个伤害效果将被取消。
那么这么万能的无敌时间是多少呢,代码层得出的结果:60tick,也就是TPS为20时的3s。(判断为60tick而不是60ms是因为下方的tick方法写到了它的自减,所以推断单位是tick)
注意:无敌时间不仅是在进入世界时起效,死亡重生之后也会有3s的无敌时间,因为死亡时系统删除了原先的Player,重生时会创建新的Player,因此存在无敌时间。


那么红框上那个isInvulnerableTo又是什么呢,翻找之后发现它和无敌时间无关,但是能解释创造模式下为什么还会受到虚空伤害。

同时通过ServerPlayer的覆盖,我们了解到变更维度过程中,也无法受到伤害。

说完了无敌时间,我们再从另一个角度——重置高度进行分析。
为了这个分析,我们就要了解摔落伤害是怎么产生的:

然而到这里,我们就找不到线索了:是谁调用了它。但是突然想起来干草块能降低摔落伤害,所以去找干草块的类(HayBlock),找到了发生伤害的地方。

通过此处,我们看到causeFallDamage的第二个参数的真面目:摔落伤害比例。Block内,这里写的是1f,而这里写为了0.2f,也就是干草块能阻止80%的摔落伤害。
同时,我们得出了摔落伤害的公式(不具有缓降效果):
当没有跳跃提升效果时:fallDamage = (fallDistance - 3.0f) * damageRatio
当存在跳跃提升效果时:fallDamage = (fallDistance - 4.0f - amplifier) * damageRatio
fallDamage-摔落伤害 fallDistance-摔落高度
damageRatio-摔落伤害比例 amplifier-跳跃提升等级
但是到了这里,线索又消失了。
由于没法通过eclipse的调用层次结构寻找,我只好用了代码探针,在Block类的fallOn那里动态插入了一个Thread.dumpStack()。

之后进行摔落,产生了两组堆栈:
Client端(不负责实体伤害)

Server端(真正需要的地方)

通过这些,我们去找ServerPlayer#doCheckFallDamage:

通过这里,我们了解到了这个方法是用于检查下方的方块的,但是还是没有出现下落距离等的出现,所以继续向下查。

这里出现了一个激动人心的字段:fallDistance,下落距离。也就是说,下落距离是根据此字段保存的,那么只要证明它被保存了,就能说明重置高度的说法是错误的。
可是LivingEntity没有定义该字段,那么这个字段就一定定义在它的上一级:Entity。

通过这一张图,我们的代码验证走到了尽头:我们可以清晰地看到这句
compoundTag.putFloat("FallDistance", this.fallDistance);
这句话意味着保存了下落高度,也就是说,在下次进入世界时,下落高度会从文件中重新读取回来,也就是你上一次退出游戏时已经下落的高度,所以说,重置高度说是错误的。
为了进行严谨的论证,下面用客户端进行验证。
2.客户端验证
首先,验证无敌时间,这个很容易验证,你在出生60tick内泡个岩浆澡就能验证:

而重置高度,就不好验证了。我想到了一种方案:生命提升后看摔落伤害——只要在超过3s的摔落高度下进行摔落测试,就能受到伤害并且能看出与不退出的关系:如果摔落之后血量有差异(误差保持在±5内),那么就证明了确实有重置高度;相反的,如果基本没有差异,那么就没有重置。
首先给予生命提升255级,把血条变成了1024滴,然后用瞬间治疗10级10秒,把心补满。之后第一次试验,从Y=1000直接摔到1,看血量。

第二次测试也是在Y=1000落下,不同的是,这次让它在Y=700左右的时候退出重进,然后再落到Y=1。


这两次摔落结果很明显,几乎没有任何血量差异。如果进行了重置,那么血量会差250滴左右。因此,我们知道了重置高度是错误的。
结论
退出重进的原理是无敌时间而非重置高度,所以下回使用这个方法的时候,还是要注意一下你距地面的高度(虽然我知道3s能下落200格高度,早死了)
一些其他的话
fallDistance其实并不是只有在掉落时才会改变,这里我们说一些和运动的内容(具体的一些东西可以去CV7593366去看),不同状态下,这些值也会改变(这里的detlaMovement是Y轴上的,简写为dY):
1.地面上的普通移动(不包括跳):dY=-0.0784000015258789(趋势位移,在实际运动中不体现)
2.水中:fallDistance逐渐变为0.025000002,dY逐渐变为-0.02500000149011622
3.飞行:dY逐渐变为±0.22500000894069672
4.水中飞行:dY逐渐变为±0.28500000759959215
5.在水中站立:dY=-0.005(水中的趋势位移)
6.水中向上移动:dY逐渐变为0.13500000685453442
7.梯子下行:fallDistance=0.15,dY=-0.22540001022815717
由于浮点数误差,小数点后四位基本上是准确极限。不过还是能从这里看出了比如说水中飞行比普通飞行快的结论。

文章中用到的Minecraft反混淆和动态修改程序包含反混淆程序(直接运行)和动态修改替换MC类的功能(原版Java Agent而非模组):https://github.com/Nickid2018/MCDynamicExchanger,该程序正在被完善中。
反编译器:Eclipse的插件,选择了CFK反编译器。
启动器:HMCL版本3.3.172
MC客户端:反混淆的1.14.4客户端,反混淆过程中不会改变代码逻辑,只有当进行Agent代理时才修改了一部分net.minecraft.commands.Commands的代码。
验证视频:

如果你有任何问题或是文章有数据和代码错误,可以在评论区留言。