「MC筛种杂谈3」如何得到54黑曜石的铁匠铺 Part 1
一、回顾
前两期中我们已经讲解了:
如何从世界种子和区域得到该区域内结构的具体位置
如何从世界种子和结构位置得到结构内箱子的具体位置
如何从世界种子和箱子位置得到该区块的装饰种子
如何从装饰种子得到箱子的lootTableSeed
如何从lootTable和lootTableSeed得到最终战利品
如果想要获取接近战利品表极限的战利品生成,用上述方法需要地毯式搜索大量种子和大量区块,这是十分消耗算力的。
然而,以LCG为原理的Java随机数生成器(rand),拥有着可逆的特性,这允许我们写出相应的算法,以rand的部分结果出发,推断rand的种子。
因此我们能够将上述过程完全反过来,仅用数秒就能得到结果。
二、相关知识介绍
你需要知道这些事实:
每个rand被初始化时都需要种子。
如果你不提供,Java会调取系统时间并进行一些运算来生成一个种子。
初始化时,有是否混淆(scramble)的选项。如果混淆,会先把种子与一个常数先异或一次,也就是说 seed ^= 25214903917L。
现在rand拥有了种子,记为seed1。
当我调用nextLong时,rand会对seed1进行一些运算,得到一个种子,记为seed2,和一个长整数long2,它通常与seed2不相等。
然后rand会将seed2设为种子,并为我的调用返回这个long2。
类似地,当我调用nextInt(bound)时,rand会对seed2进行一些运算,得到一个种子,记为seed3,和一个在值在bound之内的整数,它通常与seed3不相等。
可见,调用次数和顺序都对结果有重要影响。当我们想找符合条件的种子时,我们必须知道这个种子被调用随机数的次数和顺序。
三、实战:废弃传送门的附魔金苹果
我想获取对应着8个附魔金苹果的废弃传送门箱子的lootTableSeed。首先要分析它生成战利品时,调用随机数的情况。贴出战利品表:
首先,调用nextInt(5),且必须得到4,才能拥有8次抽奖次数。然后开始抽奖,第一次抽奖,调用nextInt(398),得到396,表明附魔金苹果。因为这一奖项并没有任何函数,第一次抽奖结束。很明显,后面七次也如此。用代码表示如下:
先创建一个dynamicProgram(这仅仅是个名字),然后加条件,然后用循环加条件。最后用dynamicProgram.reverse()就得到了一个长整数流(LongStream),熟悉Jvav的同学可以对这个流进行一些更花哨的操作,但我这里还是把它直接收集成一个List<Long>,以便解释。
运行!结果发现list是空的,这表明不存在这样的lootTableSeed,与我们概率预测相符。不过,我们仍然可以减弱条件,得到一些较接近极限的lootTableSeed。
例如:我们还是让抽奖次数是8,然后我们只让前5次抽奖是附魔金苹果,无视后面3次。我相信读者知道怎么修改程序。现在它就能够跑出一些结果。需注意,这些种子在setSeed而不scramble的情况下满足条件。因为MC在对使用lootTableSeed生成战利品时,setSeed使用了scramble,可见,我们需要将上述程序输出的“经过异或的”xoredLootTableSeed,再进行一次异或,来得到原始的lootTableSeed。(注意,异或两次相当于什么都没做,相当于还原,这是编程常识)
我在dynamicProgram后面加了个映射方法,将每个种子异或一下LCG.JAVA.multiplier,也就是前面说过的25214903917L。现在它们直接就是可用的lootTableSeed了。为了验证,我创建一个lootContext,使用list中第一个种子作为lootTableSeed,用废弃传送门的战利品表来生成战利品。可见输出了五个附魔金苹果和其它三件奖项。
下一步,既然lootTableSeed是由:
将rand设置种子为DecoratorSeed并scramble,然后nextLong
得到的,那么我们使用getSeeds方法得到一些种子,它们满足:被setSeed而不scramble的时候,其nextLong是我们输入的值(lootTableSeed)。我们称其为“经过异或的装饰种子”xoredDecoratorSeeds。
现在遍历我们上面得到的lootTableSeed的列表,对每个lootTableSeed,我们用getSeeds方法得到它对应的经过异或的装饰种子列表。
再遍历这个列表,将每个种子异或一下,得到真正的装饰种子。因为装饰种子是由族群种子(populationSeed)加上salt得到的,我们将装饰种子减去salt就得到了族群种子。注意废弃传送门的salt是40005,我们使用了一个reverseDecoratorSeed的方法来进行这一过程。
族群种子是由世界种子和区块位置完全决定的。由此我们可以遍历一些区块,并进行后继的操作。因为废弃传送门Region参数的关系,我们选取区块X坐标和区块Z坐标在0到25之间。这些位置可能生成废弃传送门,且离原点较近。
然后对族群种子,我们使用reversePopulationSeed方法来得到结构种子(structSeed)的列表。别以为到这里就结束了,事实上这些结构种子对应的该Region内的废弃传送门的坐标并不一定就是之前遍历的位置。
我们再遍历一下结构种子的列表,每次用getInRegion得到废弃传送门的位置,然后与之前遍历的位置作比较。现在它输出的种子就真正地能用了。
程序输出:
我们不妨再正向验证一下。例如对第一个74348597233418,我们用它先获取该Region内的废弃传送门的区块坐标,然后对这个区块设置populationSeed,再用populationSeed加上salt得到decoratorSeed,并设置为rand的种子,现在nextLong得到lootTableSeed,用lootContext得到战利品并输出。
可见符合五个附魔金苹果的要求。实在不放心你还可以进游戏查看。
实际上,因为每个结构种子对应65536个姐妹世界种子,它们都在相同的区块位置有着废弃传送门。然而,不同的世界种子具有不同的群系,不同的群系会影响废弃传送门的生成,得到不同群系对应的变种。这些变种可能使得废弃传送门的箱子的位置跨越区块,得到“错误的”战利品。万幸地,我们有废弃传送门的Generator,可以检查每个世界种子实际情况如何。
我们还是沿用74348597233418的结构种子,查看它的65536个姐妹世界种子。我们让高16位(up)遍历0到65536,然后把up往左移48位,再与结构种子做或运算,就得到了世界种子(worldSeed)。然后我们new一个gen,并调用generate方法。我们需要传入主世界地形生成器(overworldTerrainGenerator),而它又需要传入一个主世界群系,而这又需要世界种子和版本。我们一一传入,再给gen提供废弃传送门的坐标pos,和一个rand。
现在gen就生成完毕了,我们可以对gen调用方法来获取信息。我们调用getChestsChunkPos得到一个列表,它的每个元素都是一个pair,这个pair左边是ILootType,可以理解为战利品表,而右边是CPos,即箱子对应的区块坐标。因为废弃传送门只有一个箱子,我们get(0)得到这个箱子对应的pair,又getSecond()得到区块坐标。
现在我们知道,如果箱子在pos内,它就能生成5附魔金苹果。如果不在pos内,战利品会改变,不满足需求。所以我们再检查chestChunk与pos是否相同,如果相同就能输出世界种子了。
这个程序将会输出大约六万个世界种子。每个世界种子在区块Pos{x=12, y=0, z=2}的地方都有一个五附魔金的箱子。
注意到我们有三个不同的结构种子,可见满足我们的条件的世界种子大约有18万个。回顾一下条件:在Region(0,0)内有一个废弃传送门,使得它的箱子没有跨过结构所在的区块,而且这个区块对应的废弃传送门箱子,生成战利品时,抽奖会抽8次,其中前5次都是附魔金苹果,而不管后3次如何。
很明显,这并不是全部拥有五附魔金废门的世界种子。还有许多种子我们没有考虑到,比如允许让废门不在Region(0,0),比如允许箱子跨过区块,比如允许抽奖不是8次,比如允许抽奖前5次不全是附魔金苹果,而是所有抽奖次数加起来有5个附魔金苹果。
但我想说的是,我们关心的是存在性,只要我们找到了5附魔金苹果的种子就行了。放宽这些条件都仅仅能让数量提高几倍或者几十倍,属于量变,但无法引起质变:找到6附魔金苹果的种子。事实上,我们通过一些程序验证,几乎可以断言废门6附魔金是不存在的。
四、收尾
本来用废弃传送门举例只是为了方便理解这种逆向思考的体系的,但写到这里已经三千五百字了,所以我们将54黑曜石的实战放到下一篇讲吧。