Minecraft 1.18.2+ 中地狱堡垒的地狱砖刷怪游走问题分析

太长不看版结论

MC 1.18.2 及以后,对于地狱堡垒内部(整个外结构范围内)的堡垒特产刷怪:

  • 首次游走结束于"地狱砖"上的一次成群生成,不再能刷怪至"内结构非地狱砖"上
  • 首次游走结束于"内结构非地狱砖"上的一次成群生成,不再能刷怪至"地狱砖"上

摘自我的 b 站动态:https://t.bilibili.com/925169310311120902

现象

多名玩家在 b 站上表示,在 未破基岩 的环境下,对于使用十字路口的 凋灵玫瑰+泥土/草方块/地狱岩 的凋灵骷髅刷怪塔, 在 MC 某个未知版本以后,若使用地狱砖为刷怪塔建造了游走平台拓展,则其效率会下降

堡垒外结构 + 地狱砖能刷凋灵骷髅,这些刷怪尝试游走进刷怪平台,就应该可以提升凋灵骷髅塔的效率,但实际上效率确实下降了。乍一看,很是奇怪

相关视频/动态

一些相关的 b 站动态 / 视频:

实验验证

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

  1. 对于每个区块,选取区块中的一个随机位置,作为刷怪起始点(y 的值域上界为 heightmap),并尝试 3 次刷怪
  2. 对于每次刷怪尝试,会从刷怪起始点开始,游走 k 次。每次游走都会将坐标水平随机偏移一定距离,然后尝试生成一只怪。这 k 次游走+刷怪也被称为"成群生成"
  3. 第 1 次 游走结束后,基于当前坐标所处于的群系、结构等环境信息,获取该坐标可生成的 怪物列表,并在其中挑选一个怪物种类,作为本次成群生成的目标怪物。同时成群生成的次数 k 也将于此处,基于怪物种类确认
  4. 在每次游走结束后,将尝试在当前坐标刷一只怪。怪物是否可生成需要进行一系列的检查
    • 其中有一项检查是:确认将要生成的怪物种类,位于当前坐标的 怪物列表 里。这是用于避免例如史莱姆刷怪,游走刷到了史莱姆区块外面的情况

上述流程中,怪物列表 计算了 2 次:

  • 一次是在首次游走结束后,选取要刷的怪物时
    • 函数为 net.minecraft.world.SpawnHelper#pickRandomSpawnEntry
  • 一次是在刷怪检查过程中,确认当前要刷的怪物,是位于当前坐标的 怪物列表
    • 函数为 net.minecraft.world.SpawnHelper#containsSpawnEntry

这两次计算,都会调用 SpawnHelper 中的 getSpawnEntries 函数,来获取给定坐标的怪物列表

函数调用链

getSpawnEntries 函数中,有一个对地狱堡垒的特判:

  • shouldUseNetherFortressSpawns 函数返回 true,则直接返回 NetherFortressStructure.MONSTER_SPAWNS 属性里储存的怪物列表,即堡垒特产怪
    • shouldUseNetherFortressSpawns 函数当且仅当如果满足下方 3 个条件,则认为可使用地狱堡垒的特殊结构刷怪
      1. 要刷出的实体是类型是怪物
      2. 当前坐标下方是地狱砖
      3. 当前坐标位于地狱堡垒的外结构范围
  • 否则,调用 chunkGenerator.getEntitySpawnList,基于当前坐标所处的结构、当前群系,获取怪物列表
    • 对于地狱堡垒,坐标需要位于内结构中,才可得到堡垒特产怪

这部分特判是为了,让地狱堡垒外结构的地狱砖上,也能和保留内结构一样,可以刷出堡垒特有的怪物,如凋灵骷髅、烈焰人等

上面几个函数的源代码如下图所示:

源代码

这看上去没啥问题,只是 mojang 给地狱堡垒 + 地狱砖这种特殊刷怪情况搞了个特判而已。可惜,事实并非 mojang 所愿

地狱堡垒怪物

基于上文 刷怪流程 小节的分析,地狱堡垒的刷怪列表,有以下两种可能的取值来源

来源 地狱堡垒中的位置 条件 怪物列表取值来源
A 外结构 地狱砖上方 NetherFortressStructure.MONSTER_SPAWNS
B 内结构 非地狱砖上方 chunkGenerator.getEntitySpawnList()

内结构/外结构示意

具体的:

  • 来源 A:直接为 NetherFortressFeature.MONSTER_SPAWNS 的值
  • 来源 B:从 world.getRegistryManager() 中取得。而其中的 registry 数据,是在载入世界时, 对包含 NetherFortressFeature.MONSTER_SPAWNSBuiltinRegistries.DYNAMIC_REGISTRY_MANAGER进行深拷贝(序列化 + 反序列化)后得到的。 总而言之,来源 B 的返回值,是来源 A 的列表的深拷贝

再回到 containsSpawnEntry 函数里,就能发现有些不对劲了

List#contains

mojang 在这里使用了 java 的 List#contains 方法,来判断给定的刷怪类型是否位于当前坐标的刷怪列表中,这便是问题所在: 列表储存的元素类型 SpawnSettings.SpawnEntry 并没有实现 equals 方法,这将导致只有引用相等的元素才能返回 true

举个例子,如果用来源 A 的元素 entry 来询问来源 B 的列表 list,那 list.contains(entry) 就会返回 false。 于是,containsSpawnEntry 函数也会返回 false,刷怪尝试便因此失败

问题引入

捋一下与这个 bug 有关的相关变更节点,都是 1.18.2 的快照

  1. 22w06a, 创建 world 的 DynamicRegistryManager 时,引入了序列化+反序列化的方式,对 registry 数据进行了深拷贝
  2. 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

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