分析
Minecraft 主要用 Java 编写,史莱姆区块的生成是采用了伪随机算法。伪随机意为在同样的种子下,生成的结果是相同的。所以当有了种子之后,可以推算出几乎所有自然生成结构的位置。甚至,如果你有大量有效的自然生成结构位置,可以反推出种子。
// https://minecraft.fandom.com/wiki/Slime
import java.util.Random;
public class checkSlimechunk{
public static void main(String args[])
{
// the seed from /seed as a 64bit long literal
long seed = 12345L;
int xPosition = 123;
int zPosition = 456;
Random rnd = new Random(
// 第一部分
seed +
// 第二部分
(int) (xPosition * xPosition * 0x4c1906) +
// 第三部分
(int) (xPosition * 0x5ac0db) +
// 第四部分
(int) (zPosition * zPosition) * 0x4307a7L +
//第五部分
(int) (zPosition * 0x5f24f)
^ 0x3ad8025fL
);
System.out.println(rnd.nextInt(10) == 0);
}
}
这是wiki中扒出的游戏算法,这段代码可以理解为:伪随机种子由5部分构成,五部分相加后按位异或一个 long
值得出进入随机前的最终种子。五部分中,一共需要3个输入值,分别是 long
类种子、int
类区块x坐标、int
类区块z坐标。
- 五部分的第一部分是游戏中
/seed
获得的种子,类是long
。按照int
和long
之间加减的转换顺序和随机种子算式的优先级,所有之后的五部分都会最终转换成long
。 - 第二部分为x坐标平方再乘一个十六进制数
0x4c1906
,换算成十进制就是4987142
,在 Python 中不需要换成十进制计算,直接相乘。 - 第三部分同理。
- 第四部分注意,z平方是在
int
类中完成计算,在乘括号外的十进制时需要转化成long
。 - 最后同样也是一个
int
类计算,要注意最后的<u>按位异或的优先级在java中是比加减乘除要低的</u>,所以应该是所有部分加起来后再运算。
在RNG对象赋值好后,最后是用 .nextInt() 输出一个0-9的数字,如果数字是 0
,则该区块是史莱姆区块。
实现
框架构建
class slime(object):
def __init__(self):
self.seed = None
self.x = None
self.z = None
if __name__ == "__main__":
obj = slime()
Random Number Generator 种子计算
def javaInt(self, val):
limit = 2147483647
if not (-1*limit)-1 <= val <= limit:
val = (val+(limit+1))%(2*(limit+1))-limit-1
return val
def javaLong(self, val):
limit = 9223372036854775807
if not (-1*limit)-1 <= val <= limit:
val = (val+(limit+1))%(2*(limit+1))-limit-1
return val
Python 3中int类大小是无限的,而java中int和long都有上限。越过限制后,会从另一端继续计算,例如int的上限是 2,147,483,647,这个数+1等于-2,147,483,648,+2等于-2,147,483,647 以此类推。根据上方算法分析,坐标乘以的十六进制数算是比较大的,所以有很大可能性会过界,所以必须要有一种溢出算法能让python的不过界数模你出 Java的过界效果。
def rngSeed(self):
a = self.javaLong(self.javaInt((self.x**2)*0x4c1906))
b = self.javaLong(self.javaInt(self.x*0x5ac0db))
c = self.javaLong((self.javaInt(self.z**2)*0x4307a7))
d = self.javaLong(self.javaInt(self.z*0x5f24f))
return self.javaLong(((self.seed+a+b+c+d)^0x3ad8025f))
设置好溢出后套入算法即可,需要注意 Java的long类会在对象后加一个L和int类区分开来,在python中要把L去掉,比如 0x3ad8025fL 就变成 0x3ad8025f。
模拟 java.util.Random 算法
# https://github.com/MostAwesomeDude/java-random
class javaRandom(object):
def __init__(self):
self.seed = None
def setSeed(self, seed):
self.seed = (seed^0x5deece66d)&((1<<48)-1)
def next(self, bits):
if bits < 1:
bits = 1
elif bits > 32:
bits = 32
self.seed = (self.seed*0x5deece66d+0xb)&((1<<48)-1)
retval = self.seed>>(48-bits)
if retval & (1<<31):
retval -= (1<<32)
return retval
def nextInt(self, n):
if n > 1:
if not (n&(n-1)):
return (n*self.next(31))>>31
bits = self.next(31)
val = bits%n
while (bits-val+n-1) < 0:
bits = self.next(31)
val = bits%n
return val
参考 MostAwesomeDude 的模拟 Java 伪随机算法,去除了不需要的一些next方法,只留下了分析中需要的 .nextInt。这个类是参照官方文档对 Random() 定义写出的,并且做了Python的适配,可以直接使用。
关于 Java Random() 的介绍,参考 java.util - Class Random
结果转换
def check(self, seed, x, z):
# 输入
self.seed = seed
self.x = x
self.z = z
# 最终种子
rng = self.rngSeed()
# 初始化
obj = javaRandom()
# 设置最终种子
obj.setSeed(rng)
# 得到结果
rtn = obj.nextInt(10)
# 判断结果
if rtn == 0:
return True
else:
return False
总结
源码
class slime(object):
def __init__(self):
self.seed = None
self.x = None
self.z = None
def javaInt(self, val):
limit = 2147483647
if not (-1*limit)-1 <= val <= limit:
val = (val+(limit+1))%(2*(limit+1))-limit-1
return val
def javaLong(self, val):
limit = 9223372036854775807
if not (-1*limit)-1 <= val <= limit:
val = (val+(limit+1))%(2*(limit+1))-limit-1
return val
def rngSeed(self):
a = self.javaLong(self.javaInt((self.x**2)*0x4c1906))
b = self.javaLong(self.javaInt(self.x*0x5ac0db))
c = self.javaLong((self.javaInt(self.z**2)*0x4307a7))
d = self.javaLong(self.javaInt(self.z*0x5f24f))
return self.javaLong(((self.seed+a+b+c+d)^0x3ad8025f))
def check(self, seed, x, z):
self.seed = seed
self.x = x
self.z = z
rng = self.rngSeed()
obj = javaRandom()
obj.setSeed(rng)
rtn = obj.nextInt(10)
if rtn == 0:
return True
else:
return False
class javaRandom(object):
def __init__(self):
self.seed = None
def setSeed(self, seed):
self.seed = (seed^0x5deece66d)&((1<<48)-1)
def next(self, bits):
if bits < 1:
bits = 1
elif bits > 32:
bits = 32
self.seed = (self.seed*0x5deece66d+0xb)&((1<<48)-1)
retval = self.seed>>(48-bits)
if retval & (1<<31):
retval -= (1<<32)
return retval
def nextInt(self, n):
if n > 1:
if not (n&(n-1)):
return (n*self.next(31))>>31
bits = self.next(31)
val = bits%n
while (bits-val+n-1) < 0:
bits = self.next(31)
val = bits%n
return val
if __name__ == "__main__":
obj = slime()
exe = obj.check(seed=1664026956323281049, x=-9, z=-17)
print(exe)
返回
$ python3 demo.py
True