API
用代码生成
当声明不够用时,你的代码会在区块生成的过程中构建世界:地物、整座结构、地形的形状、哪里放哪个生物群系、洞穴,以及地面的方块。
你能写什么
| 内容 | Kotlin | CraftScript |
|---|---|---|
| 地物:尖刺、长在墙上的植物、巨石,任何放在区块里的东西 | feature("…") { place { } } |
无 |
| 跨越多个区块、用代码建造的结构 | structure("…") { plan { }; build { } } |
有(plan、build) |
| 一个维度的地形 | terrain("minecraft:the_end") { } |
有(terrain) |
| 哪里放哪个生物群系 | biomes("minecraft:the_end") { } |
有(biome_map) |
| 雕刻器:洞穴和隧道 | carver("…", chance) { } |
有(carver) |
| 逐个方块决定的生物群系地面 | biome("…") { surface { } } |
有(surface: fun(at)) |
| 方块、物品和生物群系的标签 | tag(TagKind.BIOME, "…", …) |
无 |
本页的所有代码都在区块生成时运行,并且同时在多个线程上运行。请使用提供给你的随机数(或世界的种子),而不是自己的:这样同一个种子才会生成同一个世界。在 Kotlin 中,让你的代码在多次调用之间共享的任何东西保持线程安全。在 CraftScript 中,random 的成员不可用;变化请从坐标、种子或结构的场地中取得。
地物
地物是你的代码在正在生成的区块中、在你列出的生物群系里构建的东西。位置由 Crafting 挑选,就像游戏对自己的地物那样;你的 place 函数在那里构建,构建了东西就返回 true。地物用 Kotlin 编写。
object CrystalSpikes {
private val CRYSTAL = Identifier.parse("mymod:crystal")
fun register(content: ContentBuilder) {
content.feature("crystal_spike") {
biome("#minecraft:is_overworld")
count = 2..5
heights = -54..30
on = FeatureOn.FLOOR
underground = true
step = FeatureStep.UNDERGROUND
place { world ->
val (x, y, z) = Triple(world.x, world.y, world.z)
if (!world.isIn(x, y - 1, z, "#minecraft:base_stone_overworld")) return@place false
val height = 2 + world.random.nextInt(4)
for (dy in 0 until height) {
if (!world.isAir(x, y + dy, z)) break
world.set(x, y + dy, z, CRYSTAL)
}
true
}
}
}
}| 选项 | 取值 | 默认值 |
|---|---|---|
biome(…) |
放置它的生物群系,用 id 或 #tag;至少一个 |
— |
count |
每个区块的尝试次数,一个范围:2..5 |
1..1 |
chance |
每隔这么多个区块,只在其中一个区块里生成 | 1 |
heights |
每次尝试在这个范围内的随机高度进行 | null:世界的底部(由你的代码找到位置) |
spread |
true:每次尝试在区块内随机的一列进行;false:在区块的角落,由你的代码从那里挑选 |
true |
on |
FeatureOn.FLOOR 或 CEILING:从它的高度出发,移到最多 32 格之外最近的地面或天花板旁边的空气中;找不到则不放置 |
null |
underground |
true:只在地表之下,至少低于地表 2 格(在洞穴中) |
false |
everyFloor |
true:在每个选中的列的每一层地面上进行,包括地表和每个洞穴的地面;count 是每层地面的列数 |
false |
tries |
与 everyFloor 搭配,各层地面被遍历几次:每次在每层地面上多取 count 列,所以更深的地层也能被更多次尝试触及 |
1..1 |
step |
它在填充区块的各个步骤中的哪一步生成:LAKES、SURFACE、ORES、UNDERGROUND(装饰洞穴的步骤)、VEGETATION |
VEGETATION |
使用 everyFloor 时,chance、heights 和 spread 不再生效:位置就是各层地面本身。
content.feature("glow_tufts") {
biome("mymod:crystal_glade")
everyFloor = true
count = 4..8
tries = 1..3
place { world ->
val tuft = Identifier.parse("mymod:glow_tuft")
if (world.structures(world.x, world.z).isNotEmpty()) return@place false
world.canStay(world.x, world.y, world.z, tuft) && world.set(world.x, world.y, world.z, tuft)
}
}地物能看到什么
place 得到的是正在生成的世界,视角在地物被放置的位置。它可以读取和设置正在生成的区块及其周围的区块;在它们之外设置会被跳过。在 CraftScript 中(在结构的 build 里),相同的成员用 snake_case 书写。
| 成员 | 说明 |
|---|---|
x、y、z |
它被放置的位置 |
random |
它自己的随机数:同一个种子生成同一个地物 |
seed |
世界的种子,用于整个世界保持一致的噪声 |
minY、maxY |
世界的高度;方块从 minY 向上直到 maxY 之下 |
block(x, y, z)、state(x, y, z, name)、isAir(x, y, z) |
那里的方块、它某个状态的值、是否为空气 |
isIn(x, y, z, tag) |
那里的方块是否属于某个标签("#minecraft:leaves"),或是否就是该方块("minecraft:end_stone") |
canStay(x, y, z, block, states) |
带着这些状态的方块能否留在那里:植物在它的地面上,苔藓在背向墙面的墙上。states 可以省略 |
set(x, y, z, block, states) |
设置一个方块,状态例如 mapOf("facing" to "north");超出可设置的范围,或在这里不是方块时返回 false |
inside(x, z) |
这一列是否是它可以读取和设置的 |
surface(x, z) |
最高的非空气方块上方的第一格,按地物放置之前的地形 |
top(x, z) |
同样的位置,但现在包含了此后地物所设置的内容 |
flow(x, y, z) |
区块进入世界之后,让那里的水(或岩浆,或含水的方块)流动。区块生成时设置的水是静止的,没有它就会悬在空气旁边 |
structures(x, z) |
范围包含该列所在区块的结构(id,如 minecraft:end_city),用来让地物避开它们 |
placeFile(file, x, y, z, rotation, mirror, pivotX, pivotZ) |
放置一个结构文件;见用代码放置结构文件 |
地物所指定的方块若不存在(来自未安装的模组),会被忽略,并在日志中给出一次警告;对它调用 set 会返回 false。
用代码建造的结构
对于不由文件构成、或必须贴合地形的结构,由你的代码决定它在哪里并建造它。它需要两个函数:
plan会在世界可能放置结构的每个区块中(按biomes、spacing和separation,与任何结构相同),在该区块的方块生成之前被询问:那里是否有一个。它返回它的场地:它的中心、它延伸的范围,以及建造所需的一切;若什么也不返回,那里就没有结构。build在场地覆盖的每个区块中,每次一个区块地建造属于该区块的部分。每个区块得到相同的场地和相同的随机数,所以各自建造同一座结构的那一块。场地会随世界保存,因此重启后生成的区块仍能对得上。
structure "stone_ring" {
biomes: ["#minecraft:is_overworld"]
spacing: 32
separation: 12
plan: fun(p) {
let x = p.chunk_x * 16 + 8
let z = p.chunk_z * 16 + 8
let y = p.height(x, z)
return { x: x, y: y, z: z, radius: 10, min_y: y - 8, max_y: y + 12 }
}
build: fun(world, site) {
for i in 0..15 {
let a = i * pi() / 8
let x = site.x + round(cos(a) * 10)
let z = site.z + round(sin(a) * 10)
if world.inside(x, z) {
let ground = world.surface(x, z)
for dy in 0..2 { world.set(x, ground + dy, z, "minecraft:mossy_cobblestone") }
}
}
}
}content.structure("stone_ring") {
biome("#minecraft:is_overworld")
spacing = 32
separation = 12
plan { p ->
val x = p.chunkX * 16 + 8
val z = p.chunkZ * 16 + 8
val y = p.height(x, z)
StructureSite(x, y, z, radius = 10, minY = y - 8, maxY = y + 12,
data = mapOf("tall" to p.random.nextBoolean()))
}
build { world, site ->
val stone = Identifier.parse("minecraft:mossy_cobblestone")
val top = if (site.data["tall"] == true) 4 else 2
for (i in 0 until 16) {
val x = site.x + (cos(i * PI / 8) * 10).roundToInt()
val z = site.z + (sin(i * PI / 8) * 10).roundToInt()
if (!world.inside(x, z)) continue
val ground = world.surface(x, z)
for (dy in 0..top) world.set(x, ground + dy, z, stone)
}
}
}场地(Kotlin 中是 StructureSite,CraftScript 中是一个映射):
| 字段 | 说明 | CraftScript 中的默认值 |
|---|---|---|
x、y、z |
它的中心 | 0 |
radius |
以中心为准向四周延伸多少格:决定它覆盖的区块 | 8 |
min_y、max_y(minY、maxY) |
它覆盖的高度 | y 减去和加上 radius |
data |
建造时在每个区块中建出相同结构所需的东西:数字、文本、真或假 | 空 |
在 Kotlin 中,场地还有一个由其位置生成的 seed。data 中的数字在重启后会以小数形式返回:请用 (site.data["n"] as Number).toInt() 读取。
plan 在该区块的任何方块生成之前得到的内容:
| 成员 | 说明 |
|---|---|
chunkX、chunkZ |
被询问的区块(方块 / 16) |
seed、random |
世界的种子,以及这个位置自己的随机数 |
height(x, z) |
那里的地表将有的高度(其上的第一格) |
biome(x, y, z) |
那里的生物群系 |
column(x, z)、minY |
该列中地形将有的方块,从 minY 向上到世界顶部,在它的地面、洞穴和任何建造之前(在末地:末地石和空气) |
fileSize(file) |
一个结构文件的尺寸(x、y、z),没有则为 null |
build 得到与地物相同的世界,但只能设置正在建造的那个区块中的方块,它的 random 来自场地。结构的其他适用于此的选项是 biomes、spacing、separation,以及在 Kotlin 中的 salt。用代码建造的结构需要同时有 plan 和 build。可以像其他结构一样用 /locate structure 找到它。
用代码放置结构文件
placeFile 按你的代码指定的位置放置一个结构文件,可旋转和镜像:在代码结构里放一座来自你自己文件的塔,或是地物在洞穴里放下一处遗迹。文件是模组的 structures/<path>.nbt("mymod:watchtower")或游戏自带的。只会放置位于世界可设置范围之内的部分,所以跨越多个区块的结构要从每个区块分别放置它的文件。文件里的结构方块不会被放置。如果没有这样的文件,返回 false。
val TOWER = Identifier.parse("mymod:watchtower")
content.structure("watchtower") {
biome("minecraft:plains")
spacing = 40
separation = 10
plan { p ->
val size = p.fileSize(TOWER) ?: return@plan null
val x = p.chunkX * 16 + 8
val z = p.chunkZ * 16 + 8
val y = p.height(x, z)
StructureSite(x, y, z, radius = maxOf(size.x, size.z), minY = y, maxY = y + size.y,
data = mapOf("turns" to p.random.nextInt(4), "half_x" to size.x / 2, "half_z" to size.z / 2))
}
build { world, site ->
val hx = (site.data["half_x"] as Number).toInt()
val hz = (site.data["half_z"] as Number).toInt()
// Its middle on the site, turned about its middle.
world.placeFile(TOWER, site.x - hx, site.y, site.z - hz,
rotation = (site.data["turns"] as Number).toInt(), pivotX = hx, pivotZ = hz)
}
}| 参数 | 取值 | 默认值 |
|---|---|---|
file |
文件的 id | — |
x、y、z |
它的角落放在哪里,在旋转之前 | — |
rotation |
顺时针旋转的四分之一圈数 | 0 |
mirror |
Mirror.NONE、LEFT_RIGHT(沿 z)或 FRONT_BACK(沿 x) |
NONE |
pivotX、pivotZ |
它绕文件中的哪个方块旋转 | 0, 0 |
一个维度的地形
你可以为游戏的某个维度(minecraft:overworld、minecraft:the_nether 或 minecraft:the_end)的地形赋予形状:浮空岛、起伏的山丘、遍布石柱的世界。你的形状说明每个位置有多坚实:大于 0 是陆地,小于 0 是空气。陆地由该维度通常的石头构成,其生物群系的地面铺在上面。
// Rolling hills over the whole overworld.
terrain "minecraft:overworld" {
column: fun(x, z, ys, world) {
let ground = 72 + 14 * sin(x / 60) * cos(z / 60)
return ys.map(fun(y) { return (ground - y) / 8 })
}
has_land: fun(x, z, world) { return true }
}// Round, flattened islands in the End, one every 96 blocks.
content.terrain("minecraft:the_end") { world ->
val shift = kotlin.random.Random(world.seed).nextInt(96)
object : TerrainShape {
fun across(x: Int, z: Int): Double {
val dx = (x + shift).mod(96) - 48
val dz = (z + shift).mod(96) - 48
return sqrt((dx * dx + dz * dz).toDouble())
}
override fun column(x: Int, z: Int, ys: IntArray, out: DoubleArray) {
val flat = across(x, z)
for (i in ys.indices) {
val up = (ys[i] - 64) * 2.5
out[i] = (24 - sqrt(flat * flat + up * up)) / 24
}
}
override fun hasLand(x: Int, z: Int) = across(x, z) < 24
}
}| 成员 | 说明 |
|---|---|
column(x, z, ys, out) |
x、z 这一列在 ys 的每个高度上有多坚实,按相同顺序写入 out。在 CraftScript 中,column(x, z, ys, world) 返回列表 |
hasLand(x, z)(has_land) |
这一列是否有陆地。在末地,它决定用陆地生物群系还是虚空的生物群系 |
world.seed |
世界的种子 |
world.minY、world.height |
陆地生成的高度范围:从 minY 起,向上 height 格 |
world.biome(x, y, z) |
某个方块处的生物群系 |
游戏在几格宽的单元格的角点处询问你的形状,并在它们之间做混合,所以不必精确到每个方块。在 Kotlin 中,你的函数每个世界带着它的种子被调用一次,并返回形状。你的形状生成全部陆地:游戏自己的噪声洞穴不会生成(请添加一个雕刻器),在末地,中央岛也要由你来生成。每个维度只能有一个模组来塑造它的形状。失败的形状会在日志中报告一次,并且那里不生成陆地,不会崩溃。
你自己的维度在维度页面中声明,并使用游戏的某种地形。
哪里放哪个生物群系
默认情况下,一个生物群系会占据它所取代的生物群系的位置中的一部分(share)(见世界生成)。若要自己的地图,则由你的代码挑选每个位置的生物群系。目前这用于末地(minecraft:the_end)。
biome_map "minecraft:the_end" {
biome: fun(x, y, z, land, world) {
if x * x + z * z < 65536 { return "minecraft:the_end" } // the central island
if land == "minecraft:end_highlands" and (floor(x / 64) + floor(z / 64)) % 2 == 0 {
return "mymod:crystal_glade"
}
return land
}
}content.tag(TagKind.BIOME, "glades", "crystal_glade", "ember_glade")
content.biomes("minecraft:the_end") { world ->
val glades = world.tagged("mymod:glades")
val theEnd = Identifier.parse("minecraft:the_end")
BiomeMap { x, y, z, land ->
when {
x.toLong() * x + z.toLong() * z < 256L * 256 -> theEnd // within 1024 blocks
land.path != "end_highlands" || glades.isEmpty() -> land
else -> glades[Math.floorMod((x shr 6) * 31 + (z shr 6), glades.size)]
}
}
}函数得到以 quart 为单位的 x、y、z(游戏每 4×4×4 格保存一个生物群系:一个 quart 是方块 / 4),以及 land,即游戏的噪声在那里生成的陆地类型,表示为一个生物群系:minecraft:end_highlands、end_midlands、end_barrens 或 small_end_islands。如果有你自己的形状,则在陆地上是 end_highlands,不在陆地上是 small_end_islands。在中央岛上你同样会得到 land,由你的地图自己来放置:游戏会在距中心 1024 格以内放置 minecraft:the_end。
返回的必须是末地的生物群系:游戏自带的,或你声明的并把 replaces 设为某个末地生物群系的。任何其他答案,或失败的地图,都会回退到游戏的选择(在日志中报告一次)。地图的 world 有 seed 和 tagged(tag),后者返回某个生物群系标签中的生物群系。每个维度的生物群系只能由一个模组来挑选。
雕刻器
雕刻器在地形生成之后、在它的地面和地物之前,把洞穴从陆地中挖空。对每个正在生成的区块,游戏会遍历它周围的区块(每侧 8 个);在每个区块中,该区块生物群系的每个雕刻器按它的 chance(0–1)开始,你的函数标出它在正在生成的区块中挖空的部分。被挖空的方块变成空气(在含水层之下则变成水),地面会重新铺在它们之上。生物群系通过在 carvers 中写出雕刻器的名字来使用它。
// A round room, 6 blocks across, in some chunks.
carver "round_caves" {
chance: 0.05
carve: fun(ctx) {
let cx = ctx.start_x * 16 + 8
let cz = ctx.start_z * 16 + 8
let cy = 20
for x in cx - 6..cx + 6 {
for y in cy - 6..cy + 6 {
for z in cz - 6..cz + 6 {
if (x - cx) * (x - cx) + (y - cy) * (y - cy) + (z - cz) * (z - cz) <= 36 { ctx.carve(x, y, z) }
}
}
}
}
}
biome "hollow_hills" {
replaces: "minecraft:meadow"
carvers: ["round_caves"]
}content.carver("round_caves", chance = 0.05) { ctx ->
val r = ctx.random
val cx = ctx.startX * 16 + r.nextInt(16)
val cz = ctx.startZ * 16 + r.nextInt(16)
val cy = r.nextInt(ctx.minY + 10, 40)
val radius = r.nextInt(4, 9)
for (x in cx - radius..cx + radius) for (y in cy - radius..cy + radius) for (z in cz - radius..cz + radius) {
val (dx, dy, dz) = Triple(x - cx, y - cy, z - cz)
if (dx * dx + dy * dy * 2 + dz * dz <= radius * radius) ctx.carve(x, y, z)
}
}
content.biome("hollow_hills") {
replaces = "minecraft:meadow"
carver("round_caves")
}| 成员 | 说明 |
|---|---|
startX、startZ |
这个洞穴起始所在的区块(区块坐标:方块 / 16) |
chunkX、chunkZ |
正在生成的区块 |
minY、maxY |
它可以挖空的高度,两端都包含 |
seed、random |
世界的种子,以及这个起点的随机数:在每个被询问该起点的区块中它们抽出的数相同,所以横跨区块的洞穴能对齐 |
carve(x, y, z) |
挖空那个方块,使用世界坐标;在正在生成的区块之外的方块会被跳过 |
一个洞穴从起点出发最多可延伸 8 个区块。在 CraftScript 中,chance 默认为 1。失败的雕刻器在那里什么也不挖,并在日志中报告一次。
生物群系的地面
surface 用一种方块覆盖生物群系的地面。若想要分层、斑块或斜坡,就给它一个函数:区块生成时,游戏会从上到下,对生物群系地面的每个方块询问那里该放哪种方块。没有答案则把那个方块留给维度(它的石头)。
biome "crystal_glade" {
replaces: "minecraft:end_highlands"
share: 0.3
surface: fun(at) {
if at.depth_above == 1 { return "mymod:crystal_moss" }
if at.depth_above <= 3 { return "mymod:crystal_soil" }
return null
}
}content.biome("crystal_glade") {
replaces = "minecraft:end_highlands"
share = 0.3
surface { at ->
val patch = at.noise("minecraft:surface", at.x.toDouble(), 0.0, at.z.toDouble()) > 0.2
when {
at.depthAbove == 1 && patch -> Identifier.parse("mymod:glow_moss")
at.depthAbove == 1 -> Identifier.parse("mymod:crystal_moss")
at.depthAbove <= 3 -> Identifier.parse("mymod:crystal_soil")
else -> null
}
}
}at 的成员 |
说明 |
|---|---|
x、y、z |
被询问的方块 |
depthAbove(depth_above) |
从上方开阔的空气向下数到这个方块为止的地面方块数:地面最上面的方块或洞穴地面为 1 |
depthBelow(depth_below) |
从这个方块向下数到其下方开阔空气为止的地面方块数:洞穴或虚空之上最低的那个为 1 |
surfaceDepth(surface_depth) |
这一列的表层有多深,游戏会让它有所变化 |
steep |
地面的倾斜程度是否达到游戏的山峰露出裸岩的地方那样 |
noise(id, x, y, z) |
世界的噪声之一,按名字指定("minecraft:surface"),由种子生成 |
这个函数先于 surface 起作用:函数没有给出答案的地方,如果同时给了 surface 方块,它仍然会覆盖最上层。失败的函数只会报告一次,失败的地方使用维度的地面。
标签
标签把方块、物品或生物群系归在同一个名字之下,这样地物可以检查 isIn(x, y, z, "#mymod:crystal_ground"),生物群系列表也可以写 "#mymod:glades"。模组可以向自己的标签或任何人的标签添加内容,比如游戏的:
// Blocks a crystal can grow on, yours and the game's.
content.tag(TagKind.BLOCK, "crystal_ground", "crystal_moss", "crystal_soil", "minecraft:end_stone")
// Biomes a feature or structure can name as "#mymod:glades".
content.tag(TagKind.BIOME, "glades", "crystal_glade", "ember_glade")
// Adding to the game's: a pickaxe mines it faster.
content.tag(TagKind.BLOCK, "minecraft:mineable/pickaxe", "crystal_soil")