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 编写。

content/CrystalSpikes.ktKotlin
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/GlowTufts.ktKotlin
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 在场地覆盖的每个区块中,每次一个区块地建造属于该区块的部分。每个区块得到相同的场地和相同的随机数,所以各自建造同一座结构的那一块。场地会随世界保存,因此重启后生成的区块仍能对得上。
content/stone_ring.craftCraftScript
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/StoneRing.ktKotlin
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。

content/Watchtower.ktKotlin
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 是空气。陆地由该维度通常的石头构成,其生物群系的地面铺在上面。

content/hills.craftCraftScript
// 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 }
}
content/FloatingIslands.ktKotlin
// 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)。

content/end_map.craftCraftScript
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/EndMap.ktKotlin
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 中写出雕刻器的名字来使用它。

content/round_caves.craftCraftScript
// 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/RoundCaves.ktKotlin
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 用一种方块覆盖生物群系的地面。若想要分层、斑块或斜坡,就给它一个函数:区块生成时,游戏会从上到下,对生物群系地面的每个方块询问那里该放哪种方块。没有答案则把那个方块留给维度(它的石头)。

content/crystal_glade.craftCraftScript
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/CrystalGlade.ktKotlin
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"。模组可以向自己的标签或任何人的标签添加内容,比如游戏的:

content/Tags.ktKotlin
// 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")

TagKind 是 BLOCK、ITEM 或 BIOME。值是 id 或 #tags;没有命名空间的名字属于该模组。多个模组可以向同一个标签添加内容,不存在的值会被忽略,而不是报错。

注意事项

  • 只有新区块会得到你的代码生成的内容,与世界生成中的一切相同。
  • **错误不会让世界停下。**抛出异常的代码会让它在那里正在生成的内容缺失,错误会写入日志。
  • **保持快速。**这段代码会为每个区块运行,玩家探索时要运行成千上万次;避免在其中做繁重的工作,在 CraftScript 中还要留意参考中的步数限制。