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 или #тег; хотя бы один |
— |
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)
}
}
}Участок (StructureSite в Kotlin, словарь в CraftScript):
| Поле | Что это | По умолчанию в CraftScript |
|---|---|---|
x, y, z |
его центр | 0 |
radius |
на сколько блоков вокруг центра он простирается: какие чанки он покрывает | 8 |
min_y, max_y (minY, maxY) |
высоты, которые он покрывает | y минус и плюс radius |
data |
то, что нужно постройке, чтобы построить одно и то же в каждом чанке: числа, текст, true или false | пусто |
В 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)]
}
}
}Функция получает x, y, z в квартах (игра хранит один биом на 4×4×4 блока: кварта — это блок / 4) и land, вид суши, который там создаёт шум игры, в виде биома: minecraft:end_highlands, end_midlands, end_barrens или small_end_islands. Со своей формой — end_highlands на её суше и small_end_islands вне её. land ты получаешь и на центральном острове, который твоя карта размещает сама: игра ставит minecraft:the_end в пределах 1024 блоков от центра.
Ответом должен быть биом Края: игровой или один из твоих, объявленный с 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")TagKind — это BLOCK, ITEM или BIOME. Значения — это id или #теги; имена без пространства имён принадлежат моду. В один тег могут добавлять несколько модов, а несуществующее значение пропускается, а не вызывает ошибку.
Полезно знать
- Только новые чанки получают то, что делает твой код, как и всё на странице Генерация мира.
- Ошибки не останавливают мир. Код, который падает, оставляет без того, что он создавал в этом месте, а ошибка пишется в журнал.
- Пиши быстро. Этот код выполняется для каждого чанка, тысячи раз, пока игроки исследуют мир; избегай в нём тяжёлой работы, а в CraftScript помни о лимите шагов из справочника.