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 або #тег; принаймні один —
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)
        }
    }
}

Ділянка (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, якщо такого файла немає.

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)]
        }
    }
}

Функція отримує 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.

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 або #теги; імена без простору імен належать модові. До одного тега можуть додавати кілька модів, а неіснуюче значення пропускається, а не викликає помилку.

Варто знати

  • Лише нові чанки отримують те, що робить твій код, як і все на сторінці Генерація світу.
  • Помилки не зупиняють світ. Код, що падає, залишає без того, що він створював у цьому місці, а помилка пишеться в журнал.
  • Пиши швидко. Цей код виконується для кожного чанка, тисячі разів, поки гравці досліджують світ; уникай у ньому важкої роботи, а в CraftScript пам'ятай про ліміт кроків зі довідника.