Minecraft 1.18.2+ 中地狱堡垒的地狱砖刷怪游走问题分析
太长不看版结论
MC 1.18.2 及以后,对于地狱堡垒内部(整个外结构范围内)的堡垒特产刷怪:
- 首次游走结束于"地狱砖"上的一次成群生成,不再能刷怪至"内结构非地狱砖"上
- 首次游走结束于"内结构非地狱砖"上的一次成群生成,不再能刷怪至"地狱砖"上
摘自我的 b 站动态:https://t.bilibili.com/925169310311120902
现象
多名玩家在 b 站上表示,在 未破基岩 的环境下,对于使用十字路口的 凋灵玫瑰+泥土/草方块/地狱岩 的凋灵骷髅刷怪塔, 在 MC 某个未知版本以后,若使用地狱砖为刷怪塔建造了游走平台拓展,则其效率会下降
堡垒外结构 + 地狱砖能刷凋灵骷髅,这些刷怪尝试游走进刷怪平台,就应该可以提升凋灵骷髅塔的效率,但实际上效率确实下降了。乍一看,很是奇怪
一些相关的 b 站动态 / 视频:
- As_One_ 的视频:https://www.bilibili.com/video/BV1oM4m1Q7TH/
- -小何不是河- 的动态:https://t.bilibili.com/919854760729772038
- FhFhhhhh 的动态:https://t.bilibili.com/919553893964185617
实验验证
Youmiel 对各个 MC 版本进行了大量测试,定位出这个问题的引入区间:1.18.1 ~ 1.18.2。
在 MC 1.18.1 及以前,地狱砖游走能加效率;在 MC 1.18.2 及以后,地狱砖游走反而会减效率
测试环境
为了方便得到测试数据,Youmiel 搭建了一个测试存档,环境如下:
- 灵魂沙峡谷,空置域环境,带基岩天花板
- 单十字路口凋灵骷髅塔,3 层刷怪平台,凋灵玫瑰 + 地狱岩刷怪平台。最下层刷怪平台地板的方块 Y 坐标为 69
- 使用命令方块切换如下 3 种测试场景:
- 无游走平台
- 带有 5m 的普通方块(石砖)作为游走平台
- 带有 5m 的地狱砖作为游走平台
存档:FortressTest-Minimal。已授权发布。存档版本为 MC 1.16.5
测试方式
- 使用 fabric-carpet 模组的
/spawn tracking
指令来统计凋灵骷髅的生成速率 - 每 gt 执行指令将所有凋灵骷髅 tp 出 128m,清除凋灵骷髅(存档中使用数据包实现)
- 使用存档中数据包指令
/function qc:start_test
开启测试:/tick warp
运行 180min 后,读取/spawn tracking
的输出,得到测试结果
测试结果
可以发现,在 1.18.1 和 1.18.2 之间:
- 无游走、普通方块游走,这两个场景之间的效率,及 1.18.1、1.18.2 两个版本间的效率,均无明显差距。实际数据的差距可认为是测试误差
- 在 1.18.1 里,地狱砖游走,比无游走、普通方块游走,效率 更高。符合以往对凋灵骷髅塔的预期
- 在 1.18.2 里,地狱砖游走,比无游走、普通方块游走,效率 跟低。不符合预期,但是与上面 现象 小节的表述一致
现象 小节中提到的神秘现象已复现成功,地狱砖制成的游走平台,会让凋灵骷髅塔的效率不升反降。这其中必有蹊跷
分析
刷怪流程
下面简要摘要一下自然刷怪中,刷怪循环的相关流程。注意下面流程并未包括所有实现细节,仅作为下文分析的背景。 若想要了解更多自然生成的机制,可以去看一下 五羊飞kaniol 的 b 站专栏 cv8274017
- 对于每个区块,选取区块中的一个随机位置,作为刷怪起始点(y 的值域上界为 heightmap),并尝试 3 次刷怪
- 对于每次刷怪尝试,会从刷怪起始点开始,游走 k 次。每次游走都会将坐标水平随机偏移一定距离,然后尝试生成一只怪。这 k 次游走+刷怪也被称为"成群生成"
- 在 第 1 次 游走结束后,基于当前坐标所处于的群系、结构等环境信息,获取该坐标可生成的 怪物列表,并在其中挑选一个怪物种类,作为本次成群生成的目标怪物。同时成群生成的次数 k 也将于此处,基于怪物种类确认
- 在每次游走结束后,将尝试在当前坐标刷一只怪。怪物是否可生成需要进行一系列的检查
- 其中有一项检查是:确认将要生成的怪物种类,位于当前坐标的 怪物列表 里。这是用于避免例如史莱姆刷怪,游走刷到了史莱姆区块外面的情况
上述流程中,怪物列表 计算了 2 次:
- 一次是在首次游走结束后,选取要刷的怪物时
- 函数为
net.minecraft.world.SpawnHelper#pickRandomSpawnEntry
- 函数为
- 一次是在刷怪检查过程中,确认当前要刷的怪物,是位于当前坐标的 怪物列表 里
- 函数为
net.minecraft.world.SpawnHelper#containsSpawnEntry
- 函数为
这两次计算,都会调用 SpawnHelper
中的 getSpawnEntries
函数,来获取给定坐标的怪物列表
在 getSpawnEntries
函数中,有一个对地狱堡垒的特判:
- 若
shouldUseNetherFortressSpawns
函数返回true
,则直接返回NetherFortressStructure.MONSTER_SPAWNS
属性里储存的怪物列表,即堡垒特产怪shouldUseNetherFortressSpawns
函数当且仅当如果满足下方 3 个条件,则认为可使用地狱堡垒的特殊结构刷怪- 要刷出的实体是类型是怪物
- 当前坐标下方是地狱砖
- 当前坐标位于地狱堡垒的外结构范围
- 否则,调用
chunkGenerator.getEntitySpawnList
,基于当前坐标所处的结构、当前群系,获取怪物列表- 对于地狱堡垒,坐标需要位于内结构中,才可得到堡垒特产怪
这部分特判是为了,让地狱堡垒外结构的地狱砖上,也能和保留内结构一样,可以刷出堡垒特有的怪物,如凋灵骷髅、烈焰人等
上面几个函数的源代码如下图所示:
这看上去没啥问题,只是 mojang 给地狱堡垒 + 地狱砖这种特殊刷怪情况搞了个特判而已。可惜,事实并非 mojang 所愿
地狱堡垒怪物
基于上文 刷怪流程 小节的分析,地狱堡垒的刷怪列表,有以下两种可能的取值来源
来源 | 地狱堡垒中的位置 | 条件 | 怪物列表取值来源 |
---|---|---|---|
A | 外结构 | 地狱砖上方 | NetherFortressStructure.MONSTER_SPAWNS |
B | 内结构 | 非地狱砖上方 | chunkGenerator.getEntitySpawnList() |
具体的:
- 来源 A:直接为
NetherFortressFeature.MONSTER_SPAWNS
的值 - 来源 B:从
world.getRegistryManager()
中取得。而其中的 registry 数据,是在载入世界时, 对包含NetherFortressFeature.MONSTER_SPAWNS
的BuiltinRegistries.DYNAMIC_REGISTRY_MANAGER
进行深拷贝(序列化 + 反序列化)后得到的。 总而言之,来源 B 的返回值,是来源 A 的列表的深拷贝
再回到 containsSpawnEntry
函数里,就能发现有些不对劲了
mojang 在这里使用了 java 的 List#contains
方法,来判断给定的刷怪类型是否位于当前坐标的刷怪列表中,这便是问题所在:
列表储存的元素类型 SpawnSettings.SpawnEntry
并没有实现 equals
方法,这将导致只有引用相等的元素才能返回 true
举个例子,如果用来源 A 的元素 entry
来询问来源 B 的列表 list
,那 list.contains(entry)
就会返回 false
。
于是,containsSpawnEntry
函数也会返回 false
,刷怪尝试便因此失败
问题引入
捋一下与这个 bug 有关的相关变更节点,都是 1.18.2 的快照
- 22w06a,
创建 world 的
DynamicRegistryManager
时,引入了序列化+反序列化的方式,对 registry 数据进行了深拷贝 - 1.18.2-pre1, 不再直接用 if 链来计算各种结构的刷怪列表,而将其交由数据驱动的 registry 系统管理。从此开始,来源 B 返回的列表,便成了来源 A 的深拷贝
注意,1.18.2-pre1 和 1.18.2-pre2 存在与结构刷怪相关的 bug(如 MC-248717), 因此,若在这些版本进行测试,将会得到与 1.18.1、1.18.2 均不相同的数据。1.18.2-pre3 及以后的表现,才和 1.18.2 及以后一致
上图效率的一些简单解释
- 1.18.1:地狱砖游走进刷怪平台正常刷怪,效率上升
- 1.18.2-pre1:判定过程中外结构就是内结构,哪里都能刷堡垒怪,且刷怪列表来源都是上文的来源 B,因此无游走/非地狱砖游走的效率上升至有地狱砖游走水平
- 1.18.2-pre2:判定过程中不存在外结构,因此地狱砖游走平台不影响刷怪平台,3 种场景效率都一样
- 1.18.2-pre3:外结构/内结构的判定正常了,地狱砖游走进刷怪平台无法刷怪,效率下降
- 1.18.2:同 1.18.2-pre3
结论
直接结论
在 MC 1.18.2 后,对于地狱堡垒内部(整个外结构范围内)的堡垒特产刷怪:
- 首次游走结束于"地狱砖"上的一次成群生成,不再能刷怪至"内结构非地狱砖"上
- 首次游走结束于"内结构非地狱砖"上的一次成群生成,不再能刷怪至"地狱砖"上
换而言之:
- 首次游走结束于"地狱砖"上的一次成群生成,只能把怪刷在"地狱砖"上
- 首次游走结束于"内结构非地狱砖"上的一次成群生成,只能把怪刷在"内结构非地狱砖"上
地狱砖地板、非地狱砖地板,这两类地板间再无互利共赢的关系。一道不可逾越的鸿沟已经产生了
这种现象,如果要起个名字的话,不妨称之为“地狱砖隔离性”?
现象分析
回到最开始,那些游走表现“奇怪”的凋灵骷髅塔,即符合如下条件的,常见的那种凋灵玫瑰十字路口设计:
- 基岩天花板完好
- 使用堡垒十字路口这一内结构
- 刷怪平台地板使用泥土、草方块、地狱岩等 非地狱砖 的方块
对于这一类凋灵骷髅塔,有:
- 基岩天花板的存在,使游走平台不会提供拉高 heightmap 的效果。
- 刷怪平台位于内结构,地板非地狱砖,因此要想怪物能刷在这个刷怪平台上,刷怪的那次成群生成的首次游走,必须结束于同样的非地狱砖上
- 若游走平台由地狱砖组成,则首次游走结束于这些地狱砖上的成群生成,将无法把怪刷在刷怪平台上
- 无游走相比地狱砖游走平台,多出来的那些效率,由与十字路口相接的其他堡垒内结构提供。毕竟只有内结构才能让非地狱砖上也能出堡垒怪
- 可构造一个这样的游走平台:平台位于内结构的部分用非地狱砖(如石头),其他部分用地狱砖。该场景效率将与全石头游走平台的效率一致(已实验验证)
- 如果十字路口不与任何堡垒内结构相接,则不管游走平台是否存在,游走平台地板是什么,基岩天花板是否存在,效率都是一样的
- 无游走相比地狱砖游走平台,多出来的那些效率,由与十字路口相接的其他堡垒内结构提供。毕竟只有内结构才能让非地狱砖上也能出堡垒怪
推论
在 MC 1.18.2 及以后,建造地狱堡垒怪物的刷怪塔时(如凋灵骷髅塔、烈焰人塔),在 heightmap 环境相同时,需要注意:
- 若刷怪平台的地板使用了非地狱砖,那游走平台就不能用地狱砖
- 如非更新抑制换地板的,凋灵玫瑰凋灵骷髅塔
- 只能用非地狱砖作为地板,也意味着只能利用内结构
- 若刷怪平台的地板使用了地狱砖,则游走平台的地板也得是地狱砖
- 如利用地狱堡垒外结构的凋灵骷髅塔、烈焰人塔
- 对于常见的十字路口凋灵骷髅塔,不能用地狱砖来做游走平台。与十字路口相接的内结构面积越大,可利用的游走面积也越大,这种凋灵骷髅塔效率就越高
反馈
本文所述问题已提交至 mojang 的 bug tracker:MC-271630
和以往一样,如果你想要在这个 bug report 下发表评论,请确保是在发表有价值有意义的评论。不要在评论区无意义灌水
参考
反混淆映射对照表
如果你使用的反混淆表是 mojang mapping 的话,下面是本文出现过的函数/方法/对象的对照关系,可供参考
类型 | Yarn mapping (MC 1.20.6 build.1) | Mojang mapping (MC 1.20.6) |
---|---|---|
类 | net.minecraft.registry.DynamicRegistryManager |
net.minecraft.core.RegistryAccess |
类 | net.minecraft.util.collection.Pool |
net.minecraft.util.random.WeightedRandomList |
类 | net.minecraft.world.biome.SpawnSettings.SpawnEntry |
net.minecraft.world.level.biome.MobSpawnSettings.SpawnerData |
方法 | net.minecraft.world.SpawnHelper#pickRandomSpawnEntry |
net.minecraft.world.level.NaturalSpawner#getRandomSpawnMobAt |
方法 | net.minecraft.world.SpawnHelper#containsSpawnEntry |
net.minecraft.world.level.NaturalSpawner#canSpawnMobAt |
方法 | net.minecraft.world.SpawnHelper#getSpawnEntries |
net.minecraft.world.level.NaturalSpawner#mobsAt |
方法 | net.minecraft.world.SpawnHelper#shouldUseNetherFortressSpawns |
net.minecraft.world.level.NaturalSpawner#isInNetherFortressBounds |
方法 | net.minecraft.world.World#getRegistryManager |
net.minecraft.world.level.Level#registryAccess |
方法 | net.minecraft.world.gen.chunk.ChunkGenerator#getEntitySpawnList |
net.minecraft.world.level.chunk.ChunkGenerator#getMobsAt |
属性 | net.minecraft.world.gen.structure.NetherFortressStructure#MONSTER_SPAWNS |
net.minecraft.world.level.levelgen.structure.structures.NetherFortressStructure#FORTRESS_ENEMIES |
代码
数据作图脚本:https://gist.github.com/Fallen-Breath/da87d76df3b5012ef1d93626dca96233